refactor: monorepo+pnpm (removed husky)
This commit is contained in:
216
packages/hoppscotch-app/helpers/GQLConnection.ts
Normal file
216
packages/hoppscotch-app/helpers/GQLConnection.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import { BehaviorSubject } from "rxjs"
|
||||
import {
|
||||
getIntrospectionQuery,
|
||||
buildClientSchema,
|
||||
GraphQLSchema,
|
||||
printSchema,
|
||||
GraphQLObjectType,
|
||||
GraphQLInputObjectType,
|
||||
GraphQLEnumType,
|
||||
GraphQLInterfaceType,
|
||||
} from "graphql"
|
||||
import { distinctUntilChanged, map } from "rxjs/operators"
|
||||
import { sendNetworkRequest } from "./network"
|
||||
import { GQLHeader } from "./types/HoppGQLRequest"
|
||||
|
||||
const GQL_SCHEMA_POLL_INTERVAL = 7000
|
||||
|
||||
/**
|
||||
GQLConnection deals with all the operations (like polling, schema extraction) that runs
|
||||
when a connection is made to a GraphQL server.
|
||||
*/
|
||||
export class GQLConnection {
|
||||
public isLoading$ = new BehaviorSubject<boolean>(false)
|
||||
public connected$ = new BehaviorSubject<boolean>(false)
|
||||
public schema$ = new BehaviorSubject<GraphQLSchema | null>(null)
|
||||
|
||||
public schemaString$ = this.schema$.pipe(
|
||||
distinctUntilChanged(),
|
||||
map((schema) => {
|
||||
if (!schema) return null
|
||||
|
||||
return printSchema(schema, {
|
||||
commentDescriptions: true,
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
public queryFields$ = this.schema$.pipe(
|
||||
distinctUntilChanged(),
|
||||
map((schema) => {
|
||||
if (!schema) return null
|
||||
|
||||
const fields = schema.getQueryType()?.getFields()
|
||||
if (!fields) return null
|
||||
|
||||
return Object.values(fields)
|
||||
})
|
||||
)
|
||||
|
||||
public mutationFields$ = this.schema$.pipe(
|
||||
distinctUntilChanged(),
|
||||
map((schema) => {
|
||||
if (!schema) return null
|
||||
|
||||
const fields = schema.getMutationType()?.getFields()
|
||||
if (!fields) return null
|
||||
|
||||
return Object.values(fields)
|
||||
})
|
||||
)
|
||||
|
||||
public subscriptionFields$ = this.schema$.pipe(
|
||||
distinctUntilChanged(),
|
||||
map((schema) => {
|
||||
if (!schema) return null
|
||||
|
||||
const fields = schema.getSubscriptionType()?.getFields()
|
||||
if (!fields) return null
|
||||
|
||||
return Object.values(fields)
|
||||
})
|
||||
)
|
||||
|
||||
public graphqlTypes$ = this.schema$.pipe(
|
||||
distinctUntilChanged(),
|
||||
map((schema) => {
|
||||
if (!schema) return null
|
||||
|
||||
const typeMap = schema.getTypeMap()
|
||||
|
||||
const queryTypeName = schema.getQueryType()?.name ?? ""
|
||||
const mutationTypeName = schema.getMutationType()?.name ?? ""
|
||||
const subscriptionTypeName = schema.getSubscriptionType()?.name ?? ""
|
||||
|
||||
return Object.values(typeMap).filter((type) => {
|
||||
return (
|
||||
!type.name.startsWith("__") &&
|
||||
![queryTypeName, mutationTypeName, subscriptionTypeName].includes(
|
||||
type.name
|
||||
) &&
|
||||
(type instanceof GraphQLObjectType ||
|
||||
type instanceof GraphQLInputObjectType ||
|
||||
type instanceof GraphQLEnumType ||
|
||||
type instanceof GraphQLInterfaceType)
|
||||
)
|
||||
})
|
||||
})
|
||||
)
|
||||
|
||||
private timeoutSubscription: any
|
||||
|
||||
public connect(url: string, headers: GQLHeader[]) {
|
||||
if (this.connected$.value) {
|
||||
throw new Error(
|
||||
"A connection is already running. Close it before starting another."
|
||||
)
|
||||
}
|
||||
|
||||
// Polling
|
||||
this.connected$.next(true)
|
||||
|
||||
const poll = async () => {
|
||||
await this.getSchema(url, headers)
|
||||
this.timeoutSubscription = setTimeout(() => {
|
||||
poll()
|
||||
}, GQL_SCHEMA_POLL_INTERVAL)
|
||||
}
|
||||
poll()
|
||||
}
|
||||
|
||||
public disconnect() {
|
||||
if (!this.connected$.value) {
|
||||
throw new Error("No connections are running to be disconnected")
|
||||
}
|
||||
|
||||
clearTimeout(this.timeoutSubscription)
|
||||
this.connected$.next(false)
|
||||
}
|
||||
|
||||
public reset() {
|
||||
if (this.connected$.value) this.disconnect()
|
||||
|
||||
this.isLoading$.next(false)
|
||||
this.connected$.next(false)
|
||||
this.schema$.next(null)
|
||||
}
|
||||
|
||||
private async getSchema(url: string, headers: GQLHeader[]) {
|
||||
try {
|
||||
this.isLoading$.next(true)
|
||||
|
||||
const introspectionQuery = JSON.stringify({
|
||||
query: getIntrospectionQuery(),
|
||||
})
|
||||
|
||||
const finalHeaders: Record<string, string> = {}
|
||||
headers
|
||||
.filter((x) => x.active && x.key !== "")
|
||||
.forEach((x) => (finalHeaders[x.key] = x.value))
|
||||
|
||||
const reqOptions = {
|
||||
method: "post",
|
||||
url,
|
||||
headers: {
|
||||
...finalHeaders,
|
||||
"content-type": "application/json",
|
||||
},
|
||||
data: introspectionQuery,
|
||||
}
|
||||
|
||||
const data = await sendNetworkRequest(reqOptions)
|
||||
|
||||
// HACK : Temporary trailing null character issue from the extension fix
|
||||
const response = new TextDecoder("utf-8")
|
||||
.decode(data.data)
|
||||
.replace(/\0+$/, "")
|
||||
|
||||
const introspectResponse = JSON.parse(response)
|
||||
|
||||
const schema = buildClientSchema(introspectResponse.data)
|
||||
|
||||
this.schema$.next(schema)
|
||||
|
||||
this.isLoading$.next(false)
|
||||
} catch (e: any) {
|
||||
console.error(e)
|
||||
this.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
public async runQuery(
|
||||
url: string,
|
||||
headers: GQLHeader[],
|
||||
query: string,
|
||||
variables: string
|
||||
) {
|
||||
const finalHeaders: Record<string, string> = {}
|
||||
headers
|
||||
.filter((item) => item.active && item.key !== "")
|
||||
.forEach(({ key, value }) => (finalHeaders[key] = value))
|
||||
|
||||
const parsedVariables = JSON.parse(variables || "{}")
|
||||
|
||||
const reqOptions = {
|
||||
method: "post",
|
||||
url,
|
||||
headers: {
|
||||
...headers,
|
||||
"content-type": "application/json",
|
||||
},
|
||||
data: JSON.stringify({
|
||||
query,
|
||||
variables: parsedVariables,
|
||||
}),
|
||||
}
|
||||
|
||||
const res = await sendNetworkRequest(reqOptions)
|
||||
|
||||
// HACK: Temporary trailing null character issue from the extension fix
|
||||
const responseText = new TextDecoder("utf-8")
|
||||
.decode(res.data)
|
||||
.replace(/\0+$/, "")
|
||||
|
||||
return responseText
|
||||
}
|
||||
}
|
||||
113
packages/hoppscotch-app/helpers/RESTExtURLParams.ts
Normal file
113
packages/hoppscotch-app/helpers/RESTExtURLParams.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import clone from "lodash/clone"
|
||||
import { FormDataKeyValue, HoppRESTRequest } from "./types/HoppRESTRequest"
|
||||
import { isJSONContentType } from "./utils/contenttypes"
|
||||
import { defaultRESTRequest } from "~/newstore/RESTSession"
|
||||
|
||||
/**
|
||||
* Handles translations for all the hopp.io REST Shareable URL params
|
||||
*/
|
||||
export function translateExtURLParams(
|
||||
urlParams: Record<string, any>
|
||||
): HoppRESTRequest {
|
||||
if (urlParams.v) return parseV1ExtURL(urlParams)
|
||||
else return parseV0ExtURL(urlParams)
|
||||
}
|
||||
|
||||
function parseV0ExtURL(urlParams: Record<string, any>): HoppRESTRequest {
|
||||
const resolvedReq = clone(defaultRESTRequest)
|
||||
|
||||
if (urlParams.method && typeof urlParams.method === "string") {
|
||||
resolvedReq.method = urlParams.method
|
||||
}
|
||||
|
||||
if (urlParams.url && typeof urlParams.url === "string") {
|
||||
if (urlParams.path && typeof urlParams.path === "string") {
|
||||
resolvedReq.endpoint = `${urlParams.url}/${urlParams.path}`
|
||||
} else {
|
||||
resolvedReq.endpoint = urlParams.url
|
||||
}
|
||||
}
|
||||
|
||||
if (urlParams.headers && typeof urlParams.headers === "string") {
|
||||
resolvedReq.headers = JSON.parse(urlParams.headers)
|
||||
}
|
||||
|
||||
if (urlParams.params && typeof urlParams.params === "string") {
|
||||
resolvedReq.params = JSON.parse(urlParams.params)
|
||||
}
|
||||
|
||||
if (urlParams.httpUser && typeof urlParams.httpUser === "string") {
|
||||
resolvedReq.auth = {
|
||||
authType: "basic",
|
||||
authActive: true,
|
||||
username: urlParams.httpUser,
|
||||
password: urlParams.httpPassword ?? "",
|
||||
}
|
||||
}
|
||||
|
||||
if (urlParams.bearerToken && typeof urlParams.bearerToken === "string") {
|
||||
resolvedReq.auth = {
|
||||
authType: "bearer",
|
||||
authActive: true,
|
||||
token: urlParams.bearerToken,
|
||||
}
|
||||
}
|
||||
|
||||
if (urlParams.contentType) {
|
||||
if (urlParams.contentType === "multipart/form-data") {
|
||||
resolvedReq.body = {
|
||||
contentType: "multipart/form-data",
|
||||
body: JSON.parse(urlParams.bodyParams || "[]").map(
|
||||
(x: any) =>
|
||||
<FormDataKeyValue>{
|
||||
active: x.active,
|
||||
key: x.key,
|
||||
value: x.value,
|
||||
isFile: false,
|
||||
}
|
||||
),
|
||||
}
|
||||
} else if (isJSONContentType(urlParams.contentType)) {
|
||||
if (urlParams.rawParams) {
|
||||
resolvedReq.body = {
|
||||
contentType: urlParams.contentType,
|
||||
body: urlParams.rawParams,
|
||||
}
|
||||
} else {
|
||||
resolvedReq.body = {
|
||||
contentType: urlParams.contentType,
|
||||
body: urlParams.bodyParams,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
resolvedReq.body = {
|
||||
contentType: urlParams.contentType,
|
||||
body: urlParams.rawParams,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resolvedReq
|
||||
}
|
||||
|
||||
function parseV1ExtURL(urlParams: Record<string, any>): HoppRESTRequest {
|
||||
const resolvedReq = clone(defaultRESTRequest)
|
||||
|
||||
if (urlParams.headers && typeof urlParams.headers === "string") {
|
||||
resolvedReq.headers = JSON.parse(urlParams.headers)
|
||||
}
|
||||
|
||||
if (urlParams.params && typeof urlParams.params === "string") {
|
||||
resolvedReq.params = JSON.parse(urlParams.params)
|
||||
}
|
||||
|
||||
if (urlParams.method && typeof urlParams.method === "string") {
|
||||
resolvedReq.method = urlParams.method
|
||||
}
|
||||
|
||||
if (urlParams.endpoint && typeof urlParams.endpoint === "string") {
|
||||
resolvedReq.endpoint = urlParams.endpoint
|
||||
}
|
||||
|
||||
return resolvedReq
|
||||
}
|
||||
155
packages/hoppscotch-app/helpers/RequestRunner.ts
Normal file
155
packages/hoppscotch-app/helpers/RequestRunner.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { Observable } from "rxjs"
|
||||
import { filter } from "rxjs/operators"
|
||||
import getEnvironmentVariablesFromScript from "./preRequest"
|
||||
import { getEffectiveRESTRequest } from "./utils/EffectiveURL"
|
||||
import { HoppRESTResponse } from "./types/HoppRESTResponse"
|
||||
import { createRESTNetworkRequestStream } from "./network"
|
||||
import runTestScriptWithVariables, {
|
||||
transformResponseForTesting,
|
||||
} from "./postwomanTesting"
|
||||
import { HoppTestData, HoppTestResult } from "./types/HoppTestResult"
|
||||
import { getRESTRequest, setRESTTestResults } from "~/newstore/RESTSession"
|
||||
|
||||
/**
|
||||
* Runs a REST network request along with all the
|
||||
* other side processes (like running test scripts)
|
||||
*/
|
||||
export function runRESTRequest$(): Observable<HoppRESTResponse> {
|
||||
const envs = getEnvironmentVariablesFromScript(
|
||||
getRESTRequest().preRequestScript
|
||||
)
|
||||
|
||||
const effectiveRequest = getEffectiveRESTRequest(getRESTRequest(), {
|
||||
name: "Env",
|
||||
variables: Object.keys(envs).map((key) => {
|
||||
return {
|
||||
key,
|
||||
value: envs[key],
|
||||
}
|
||||
}),
|
||||
})
|
||||
|
||||
const stream = createRESTNetworkRequestStream(effectiveRequest)
|
||||
|
||||
// Run Test Script when request ran successfully
|
||||
const subscription = stream
|
||||
.pipe(filter((res) => res.type === "success"))
|
||||
.subscribe((res) => {
|
||||
const testReport: {
|
||||
report: "" // ¯\_(ツ)_/¯
|
||||
testResults: Array<
|
||||
| {
|
||||
result: "FAIL"
|
||||
message: string
|
||||
styles: { icon: "close"; class: "cl-error-response" }
|
||||
}
|
||||
| {
|
||||
result: "PASS"
|
||||
message: string
|
||||
styles: { icon: "check"; class: "success-response" }
|
||||
}
|
||||
| { startBlock: string; styles: { icon: ""; class: "" } }
|
||||
| { endBlock: true; styles: { icon: ""; class: "" } }
|
||||
>
|
||||
errors: [] // ¯\_(ツ)_/¯
|
||||
} = runTestScriptWithVariables(effectiveRequest.testScript, {
|
||||
response: transformResponseForTesting(res),
|
||||
}) as any
|
||||
|
||||
setRESTTestResults(translateToNewTestResults(testReport))
|
||||
|
||||
subscription.unsubscribe()
|
||||
})
|
||||
|
||||
return stream
|
||||
}
|
||||
|
||||
function isTestPass(x: any): x is {
|
||||
result: "PASS"
|
||||
styles: { icon: "check"; class: "success-response" }
|
||||
} {
|
||||
return x.result !== undefined && x.result === "PASS"
|
||||
}
|
||||
|
||||
function isTestFail(x: any): x is {
|
||||
result: "FAIL"
|
||||
message: string
|
||||
styles: { icon: "close"; class: "cl-error-response" }
|
||||
} {
|
||||
return x.result !== undefined && x.result === "FAIL"
|
||||
}
|
||||
|
||||
function isStartBlock(
|
||||
x: any
|
||||
): x is { startBlock: string; styles: { icon: ""; class: "" } } {
|
||||
return x.startBlock !== undefined
|
||||
}
|
||||
|
||||
function isEndBlock(
|
||||
x: any
|
||||
): x is { endBlock: true; styles: { icon: ""; class: "" } } {
|
||||
return x.endBlock !== undefined
|
||||
}
|
||||
|
||||
function translateToNewTestResults(testReport: {
|
||||
report: "" // ¯\_(ツ)_/¯
|
||||
testResults: Array<
|
||||
| {
|
||||
result: "FAIL"
|
||||
message: string
|
||||
styles: { icon: "close"; class: "cl-error-response" }
|
||||
}
|
||||
| {
|
||||
result: "PASS"
|
||||
message: string
|
||||
styles: { icon: "check"; class: "success-response" }
|
||||
}
|
||||
| { startBlock: string; styles: { icon: ""; class: "" } }
|
||||
| { endBlock: true; styles: { icon: ""; class: "" } }
|
||||
>
|
||||
errors: [] // ¯\_(ツ)_/¯
|
||||
}): HoppTestResult {
|
||||
// Build a stack of test data which we eventually build up based on the results
|
||||
const testsStack: HoppTestData[] = [
|
||||
{
|
||||
description: "root",
|
||||
tests: [],
|
||||
expectResults: [],
|
||||
},
|
||||
]
|
||||
|
||||
testReport.testResults.forEach((result) => {
|
||||
// This is a test block start, push an empty test to the stack
|
||||
if (isStartBlock(result)) {
|
||||
testsStack.push({
|
||||
description: result.startBlock,
|
||||
tests: [],
|
||||
expectResults: [],
|
||||
})
|
||||
} else if (isEndBlock(result)) {
|
||||
// End of the block, pop the stack and add it as a child to the current stack top
|
||||
const testData = testsStack.pop()!
|
||||
testsStack[testsStack.length - 1].tests.push(testData)
|
||||
} else if (isTestPass(result)) {
|
||||
// A normal PASS expectation
|
||||
testsStack[testsStack.length - 1].expectResults.push({
|
||||
status: "pass",
|
||||
message: result.message,
|
||||
})
|
||||
} else if (isTestFail(result)) {
|
||||
// A normal FAIL expectation
|
||||
testsStack[testsStack.length - 1].expectResults.push({
|
||||
status: "fail",
|
||||
message: result.message,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// We should end up with only the root stack entry
|
||||
if (testsStack.length !== 1) throw new Error("Invalid test result structure")
|
||||
|
||||
return {
|
||||
expectResults: testsStack[0].expectResults,
|
||||
tests: testsStack[0].tests,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
import { getEditorLangForMimeType } from "../editorutils"
|
||||
|
||||
describe("getEditorLangForMimeType", () => {
|
||||
test("returns 'json' for valid JSON mimes", () => {
|
||||
expect(getEditorLangForMimeType("application/json")).toMatch("json")
|
||||
expect(getEditorLangForMimeType("application/hal+json")).toMatch("json")
|
||||
expect(getEditorLangForMimeType("application/vnd.api+json")).toMatch("json")
|
||||
})
|
||||
|
||||
test("returns 'xml' for valid XML mimes", () => {
|
||||
expect(getEditorLangForMimeType("application/xml")).toMatch("xml")
|
||||
})
|
||||
|
||||
test("returns 'html' for valid HTML mimes", () => {
|
||||
expect(getEditorLangForMimeType("text/html")).toMatch("html")
|
||||
})
|
||||
|
||||
test("returns 'plain_text' for plain text mime", () => {
|
||||
expect(getEditorLangForMimeType("text/plain")).toMatch("plain_text")
|
||||
})
|
||||
|
||||
test("returns 'plain_text' for unimplemented mimes", () => {
|
||||
expect(getEditorLangForMimeType("image/gif")).toMatch("plain_text")
|
||||
})
|
||||
|
||||
test("returns 'plain_text' for null/undefined mimes", () => {
|
||||
expect(getEditorLangForMimeType(null)).toMatch("plain_text")
|
||||
expect(getEditorLangForMimeType(undefined)).toMatch("plain_text")
|
||||
})
|
||||
})
|
||||
34
packages/hoppscotch-app/helpers/__tests__/jsonParse.spec.js
Normal file
34
packages/hoppscotch-app/helpers/__tests__/jsonParse.spec.js
Normal file
@@ -0,0 +1,34 @@
|
||||
import jsonParse from "../jsonParse"
|
||||
|
||||
describe("jsonParse", () => {
|
||||
test("parses without errors for valid JSON", () => {
|
||||
const testJSON = JSON.stringify({
|
||||
name: "hoppscotch",
|
||||
url: "https://hoppscotch.io",
|
||||
awesome: true,
|
||||
when: 2019,
|
||||
})
|
||||
|
||||
expect(() => jsonParse(testJSON)).not.toThrow()
|
||||
})
|
||||
|
||||
test("throws error for invalid JSON", () => {
|
||||
const testJSON = '{ "name": hopp "url": true }'
|
||||
|
||||
expect(() => jsonParse(testJSON)).toThrow()
|
||||
})
|
||||
|
||||
test("thrown error has proper info fields", () => {
|
||||
expect.assertions(3)
|
||||
|
||||
const testJSON = '{ "name": hopp "url": true }'
|
||||
|
||||
try {
|
||||
jsonParse(testJSON)
|
||||
} catch (e) {
|
||||
expect(e).toHaveProperty("start")
|
||||
expect(e).toHaveProperty("end")
|
||||
expect(e).toHaveProperty("message")
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,84 @@
|
||||
import { cancelRunningRequest, sendNetworkRequest } from "../network"
|
||||
|
||||
import AxiosStrategy, {
|
||||
cancelRunningAxiosRequest,
|
||||
} from "../strategies/AxiosStrategy"
|
||||
import ExtensionStrategy, {
|
||||
cancelRunningExtensionRequest,
|
||||
hasExtensionInstalled,
|
||||
} from "../strategies/ExtensionStrategy"
|
||||
|
||||
jest.mock("../strategies/AxiosStrategy", () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(() => Promise.resolve()),
|
||||
cancelRunningAxiosRequest: jest.fn(() => Promise.resolve()),
|
||||
}))
|
||||
|
||||
jest.mock("../strategies/ExtensionStrategy", () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(() => Promise.resolve()),
|
||||
cancelRunningExtensionRequest: jest.fn(() => Promise.resolve()),
|
||||
hasExtensionInstalled: jest.fn(),
|
||||
}))
|
||||
|
||||
jest.mock("~/newstore/settings", () => {
|
||||
return {
|
||||
settingsStore: {
|
||||
value: {
|
||||
EXTENSIONS_ENABLED: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
global.$nuxt = {
|
||||
$loading: {
|
||||
finish: jest.fn(() => Promise.resolve()),
|
||||
},
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks() // Reset the call count for the mock functions
|
||||
})
|
||||
|
||||
describe("cancelRunningRequest", () => {
|
||||
test("cancels only axios request if extension not allowed in settings and extension is installed", () => {
|
||||
hasExtensionInstalled.mockReturnValue(true)
|
||||
|
||||
cancelRunningRequest()
|
||||
|
||||
expect(cancelRunningExtensionRequest).not.toHaveBeenCalled()
|
||||
expect(cancelRunningAxiosRequest).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("cancels only axios request if extension is not allowed and not installed", () => {
|
||||
hasExtensionInstalled.mockReturnValue(false)
|
||||
|
||||
cancelRunningRequest()
|
||||
|
||||
expect(cancelRunningExtensionRequest).not.toHaveBeenCalled()
|
||||
expect(cancelRunningAxiosRequest).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("sendNetworkRequest", () => {
|
||||
test("runs only axios request if extension not allowed in settings and extension is installed and clears the progress bar", async () => {
|
||||
hasExtensionInstalled.mockReturnValue(true)
|
||||
|
||||
await sendNetworkRequest({})
|
||||
|
||||
expect(ExtensionStrategy).not.toHaveBeenCalled()
|
||||
expect(AxiosStrategy).toHaveBeenCalled()
|
||||
expect(global.$nuxt.$loading.finish).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("runs only axios request if extension is not allowed and not installed and clears the progress bar", async () => {
|
||||
hasExtensionInstalled.mockReturnValue(false)
|
||||
|
||||
await sendNetworkRequest({})
|
||||
|
||||
expect(ExtensionStrategy).not.toHaveBeenCalled()
|
||||
expect(AxiosStrategy).toHaveBeenCalled()
|
||||
expect(global.$nuxt.$loading.finish).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,83 @@
|
||||
import { cancelRunningRequest, sendNetworkRequest } from "../network"
|
||||
|
||||
import AxiosStrategy, {
|
||||
cancelRunningAxiosRequest,
|
||||
} from "../strategies/AxiosStrategy"
|
||||
import ExtensionStrategy, {
|
||||
cancelRunningExtensionRequest,
|
||||
hasExtensionInstalled,
|
||||
} from "../strategies/ExtensionStrategy"
|
||||
|
||||
jest.mock("../strategies/AxiosStrategy", () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(() => Promise.resolve()),
|
||||
cancelRunningAxiosRequest: jest.fn(() => Promise.resolve()),
|
||||
}))
|
||||
|
||||
jest.mock("../strategies/ExtensionStrategy", () => ({
|
||||
__esModule: true,
|
||||
default: jest.fn(() => Promise.resolve()),
|
||||
cancelRunningExtensionRequest: jest.fn(() => Promise.resolve()),
|
||||
hasExtensionInstalled: jest.fn(),
|
||||
}))
|
||||
|
||||
jest.mock("~/newstore/settings", () => {
|
||||
return {
|
||||
settingsStore: {
|
||||
value: {
|
||||
EXTENSIONS_ENABLED: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
global.$nuxt = {
|
||||
$loading: {
|
||||
finish: jest.fn(() => Promise.resolve()),
|
||||
},
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks() // Reset the call count for the mock functions
|
||||
})
|
||||
|
||||
describe("cancelRunningRequest", () => {
|
||||
test("cancels only extension request if extension allowed in settings and is installed", () => {
|
||||
hasExtensionInstalled.mockReturnValue(true)
|
||||
|
||||
cancelRunningRequest()
|
||||
|
||||
expect(cancelRunningAxiosRequest).not.toHaveBeenCalled()
|
||||
expect(cancelRunningExtensionRequest).toHaveBeenCalled()
|
||||
})
|
||||
test("cancels only axios request if extension is allowed but not installed", () => {
|
||||
hasExtensionInstalled.mockReturnValue(false)
|
||||
|
||||
cancelRunningRequest()
|
||||
|
||||
expect(cancelRunningExtensionRequest).not.toHaveBeenCalled()
|
||||
expect(cancelRunningAxiosRequest).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("sendNetworkRequest", () => {
|
||||
test("runs only extension request if extension allowed in settings and is installed and clears the progress bar", async () => {
|
||||
hasExtensionInstalled.mockReturnValue(true)
|
||||
|
||||
await sendNetworkRequest({})
|
||||
|
||||
expect(AxiosStrategy).not.toHaveBeenCalled()
|
||||
expect(ExtensionStrategy).toHaveBeenCalled()
|
||||
expect(global.$nuxt.$loading.finish).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("runs only axios request if extension is allowed but not installed and clears the progress bar", async () => {
|
||||
hasExtensionInstalled.mockReturnValue(false)
|
||||
|
||||
await sendNetworkRequest({})
|
||||
|
||||
expect(ExtensionStrategy).not.toHaveBeenCalled()
|
||||
expect(AxiosStrategy).toHaveBeenCalled()
|
||||
expect(global.$nuxt.$loading.finish).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,42 @@
|
||||
import { getPlatformSpecialKey } from "../platformutils"
|
||||
|
||||
describe("getPlatformSpecialKey", () => {
|
||||
let platformGetter
|
||||
|
||||
beforeEach(() => {
|
||||
platformGetter = jest.spyOn(navigator, "platform", "get")
|
||||
})
|
||||
|
||||
test("returns '⌘' for Apple platforms", () => {
|
||||
platformGetter.mockReturnValue("Mac")
|
||||
expect(getPlatformSpecialKey()).toMatch("⌘")
|
||||
|
||||
platformGetter.mockReturnValue("iPhone")
|
||||
expect(getPlatformSpecialKey()).toMatch("⌘")
|
||||
|
||||
platformGetter.mockReturnValue("iPad")
|
||||
expect(getPlatformSpecialKey()).toMatch("⌘")
|
||||
|
||||
platformGetter.mockReturnValue("iPod")
|
||||
expect(getPlatformSpecialKey()).toMatch("⌘")
|
||||
})
|
||||
|
||||
test("return 'Ctrl' for non-Apple platforms", () => {
|
||||
platformGetter.mockReturnValue("Android")
|
||||
expect(getPlatformSpecialKey()).toMatch("Ctrl")
|
||||
|
||||
platformGetter.mockReturnValue("Windows")
|
||||
expect(getPlatformSpecialKey()).toMatch("Ctrl")
|
||||
|
||||
platformGetter.mockReturnValue("Linux")
|
||||
expect(getPlatformSpecialKey()).toMatch("Ctrl")
|
||||
})
|
||||
|
||||
test("returns 'Ctrl' for null/undefined platforms", () => {
|
||||
platformGetter.mockReturnValue(null)
|
||||
expect(getPlatformSpecialKey()).toMatch("Ctrl")
|
||||
|
||||
platformGetter.mockReturnValue(undefined)
|
||||
expect(getPlatformSpecialKey()).toMatch("Ctrl")
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,313 @@
|
||||
import runTestScriptWithVariables from "../postwomanTesting"
|
||||
|
||||
/**
|
||||
* @param {string} script
|
||||
* @param {number} index
|
||||
*/
|
||||
function getTestResult(script, index) {
|
||||
return runTestScriptWithVariables(script).testResults[index].result
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} script
|
||||
*/
|
||||
function getErrors(script) {
|
||||
return runTestScriptWithVariables(script).errors
|
||||
}
|
||||
|
||||
describe("Error handling", () => {
|
||||
test("throws error at unknown test method", () => {
|
||||
const testScriptWithUnknownMethod = "pw.expect(1).toBeSomeUnknownMethod()"
|
||||
expect(() => {
|
||||
runTestScriptWithVariables(testScriptWithUnknownMethod)
|
||||
}).toThrow()
|
||||
})
|
||||
test("errors array is empty on a successful test", () => {
|
||||
expect(getErrors("pw.expect(1).toBe(1)")).toStrictEqual([])
|
||||
})
|
||||
test("throws error at a variable which is not declared", () => {
|
||||
expect(() => {
|
||||
runTestScriptWithVariables("someVariable")
|
||||
}).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("toBe", () => {
|
||||
test("test for numbers", () => {
|
||||
expect(getTestResult("pw.expect(1).toBe(2)", 0)).toEqual("FAIL")
|
||||
|
||||
expect(getTestResult("pw.expect(1).toBe(1)", 0)).toEqual("PASS")
|
||||
})
|
||||
|
||||
test("test for strings", () => {
|
||||
expect(getTestResult("pw.expect('hello').toBe('bonjour')", 0)).toEqual(
|
||||
"FAIL"
|
||||
)
|
||||
expect(getTestResult("pw.expect('hi').toBe('hi')", 0)).toEqual("PASS")
|
||||
})
|
||||
|
||||
test("test for negative assertion (.not.toBe)", () => {
|
||||
expect(getTestResult("pw.expect(1).not.toBe(1)", 0)).toEqual("FAIL")
|
||||
expect(getTestResult("pw.expect(1).not.toBe(2)", 0)).toEqual("PASS")
|
||||
expect(getTestResult("pw.expect('world').not.toBe('planet')", 0)).toEqual(
|
||||
"PASS"
|
||||
)
|
||||
expect(getTestResult("pw.expect('world').not.toBe('world')", 0)).toEqual(
|
||||
"FAIL"
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("toHaveProperty", () => {
|
||||
const dummyResponse = {
|
||||
id: 843,
|
||||
description: "random",
|
||||
}
|
||||
|
||||
test("test for positive assertion (.toHaveProperty)", () => {
|
||||
expect(
|
||||
getTestResult(
|
||||
`pw.expect(${JSON.stringify(dummyResponse)}).toHaveProperty("id")`,
|
||||
0
|
||||
)
|
||||
).toEqual("PASS")
|
||||
expect(
|
||||
getTestResult(`pw.expect(${dummyResponse.id}).toBe(843)`, 0)
|
||||
).toEqual("PASS")
|
||||
})
|
||||
test("test for negative assertion (.not.toHaveProperty)", () => {
|
||||
expect(
|
||||
getTestResult(
|
||||
`pw.expect(${JSON.stringify(
|
||||
dummyResponse
|
||||
)}).not.toHaveProperty("type")`,
|
||||
0
|
||||
)
|
||||
).toEqual("PASS")
|
||||
expect(
|
||||
getTestResult(
|
||||
`pw.expect(${JSON.stringify(dummyResponse)}).toHaveProperty("type")`,
|
||||
0
|
||||
)
|
||||
).toEqual("FAIL")
|
||||
})
|
||||
})
|
||||
|
||||
describe("toBeLevel2xx", () => {
|
||||
test("test for numbers", () => {
|
||||
expect(getTestResult("pw.expect(200).toBeLevel2xx()", 0)).toEqual("PASS")
|
||||
expect(getTestResult("pw.expect(200).not.toBeLevel2xx()", 0)).toEqual(
|
||||
"FAIL"
|
||||
)
|
||||
expect(getTestResult("pw.expect(300).toBeLevel2xx()", 0)).toEqual("FAIL")
|
||||
expect(getTestResult("pw.expect(300).not.toBeLevel2xx()", 0)).toEqual(
|
||||
"PASS"
|
||||
)
|
||||
})
|
||||
test("test for strings", () => {
|
||||
expect(getTestResult("pw.expect('200').toBeLevel2xx()", 0)).toEqual("PASS")
|
||||
expect(getTestResult("pw.expect('200').not.toBeLevel2xx()", 0)).toEqual(
|
||||
"FAIL"
|
||||
)
|
||||
expect(getTestResult("pw.expect('300').toBeLevel2xx()", 0)).toEqual("FAIL")
|
||||
expect(getTestResult("pw.expect('300').not.toBeLevel2xx()", 0)).toEqual(
|
||||
"PASS"
|
||||
)
|
||||
})
|
||||
test("failed to parse to integer", () => {
|
||||
expect(getTestResult("pw.expect(undefined).toBeLevel2xx()", 0)).toEqual(
|
||||
"FAIL"
|
||||
)
|
||||
expect(getTestResult("pw.expect(null).toBeLevel2xx()", 0)).toEqual("FAIL")
|
||||
expect(() => {
|
||||
runTestScriptWithVariables("pw.expect(Symbol('test')).toBeLevel2xx()")
|
||||
}).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("toBeLevel3xx()", () => {
|
||||
test("test for numbers", () => {
|
||||
expect(getTestResult("pw.expect(300).toBeLevel3xx()", 0)).toEqual("PASS")
|
||||
expect(getTestResult("pw.expect(300).not.toBeLevel3xx()", 0)).toEqual(
|
||||
"FAIL"
|
||||
)
|
||||
expect(getTestResult("pw.expect(400).toBeLevel3xx()", 0)).toEqual("FAIL")
|
||||
expect(getTestResult("pw.expect(400).not.toBeLevel3xx()", 0)).toEqual(
|
||||
"PASS"
|
||||
)
|
||||
})
|
||||
test("test for strings", () => {
|
||||
expect(getTestResult("pw.expect('300').toBeLevel3xx()", 0)).toEqual("PASS")
|
||||
expect(getTestResult("pw.expect('300').not.toBeLevel3xx()", 0)).toEqual(
|
||||
"FAIL"
|
||||
)
|
||||
expect(getTestResult("pw.expect('400').toBeLevel3xx()", 0)).toEqual("FAIL")
|
||||
expect(getTestResult("pw.expect('400').not.toBeLevel3xx()", 0)).toEqual(
|
||||
"PASS"
|
||||
)
|
||||
})
|
||||
test("failed to parse to integer", () => {
|
||||
expect(getTestResult("pw.expect(undefined).toBeLevel3xx()", 0)).toEqual(
|
||||
"FAIL"
|
||||
)
|
||||
expect(getTestResult("pw.expect(null).toBeLevel3xx()", 0)).toEqual("FAIL")
|
||||
expect(() => {
|
||||
runTestScriptWithVariables("pw.expect(Symbol('test')).toBeLevel3xx()")
|
||||
}).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("toBeLevel4xx()", () => {
|
||||
test("test for numbers", () => {
|
||||
expect(getTestResult("pw.expect(400).toBeLevel4xx()", 0)).toEqual("PASS")
|
||||
expect(getTestResult("pw.expect(400).not.toBeLevel4xx()", 0)).toEqual(
|
||||
"FAIL"
|
||||
)
|
||||
expect(getTestResult("pw.expect(500).toBeLevel4xx()", 0)).toEqual("FAIL")
|
||||
expect(getTestResult("pw.expect(500).not.toBeLevel4xx()", 0)).toEqual(
|
||||
"PASS"
|
||||
)
|
||||
})
|
||||
test("test for strings", () => {
|
||||
expect(getTestResult("pw.expect('400').toBeLevel4xx()", 0)).toEqual("PASS")
|
||||
expect(getTestResult("pw.expect('400').not.toBeLevel4xx()", 0)).toEqual(
|
||||
"FAIL"
|
||||
)
|
||||
expect(getTestResult("pw.expect('500').toBeLevel4xx()", 0)).toEqual("FAIL")
|
||||
expect(getTestResult("pw.expect('500').not.toBeLevel4xx()", 0)).toEqual(
|
||||
"PASS"
|
||||
)
|
||||
})
|
||||
test("failed to parse to integer", () => {
|
||||
expect(getTestResult("pw.expect(undefined).toBeLevel4xx()", 0)).toEqual(
|
||||
"FAIL"
|
||||
)
|
||||
expect(getTestResult("pw.expect(null).toBeLevel4xx()", 0)).toEqual("FAIL")
|
||||
expect(() => {
|
||||
runTestScriptWithVariables("pw.expect(Symbol('test')).toBeLevel4xx()")
|
||||
}).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("toBeLevel5xx()", () => {
|
||||
test("test for numbers", () => {
|
||||
expect(getTestResult("pw.expect(500).toBeLevel5xx()", 0)).toEqual("PASS")
|
||||
expect(getTestResult("pw.expect(500).not.toBeLevel5xx()", 0)).toEqual(
|
||||
"FAIL"
|
||||
)
|
||||
expect(getTestResult("pw.expect(200).toBeLevel5xx()", 0)).toEqual("FAIL")
|
||||
expect(getTestResult("pw.expect(200).not.toBeLevel5xx()", 0)).toEqual(
|
||||
"PASS"
|
||||
)
|
||||
})
|
||||
test("test for strings", () => {
|
||||
expect(getTestResult("pw.expect('500').toBeLevel5xx()", 0)).toEqual("PASS")
|
||||
expect(getTestResult("pw.expect('500').not.toBeLevel5xx()", 0)).toEqual(
|
||||
"FAIL"
|
||||
)
|
||||
expect(getTestResult("pw.expect('200').toBeLevel5xx()", 0)).toEqual("FAIL")
|
||||
expect(getTestResult("pw.expect('200').not.toBeLevel5xx()", 0)).toEqual(
|
||||
"PASS"
|
||||
)
|
||||
})
|
||||
test("failed to parse to integer", () => {
|
||||
expect(getTestResult("pw.expect(undefined).toBeLevel5xx()", 0)).toEqual(
|
||||
"FAIL"
|
||||
)
|
||||
expect(getTestResult("pw.expect(null).toBeLevel5xx()", 0)).toEqual("FAIL")
|
||||
expect(() => {
|
||||
runTestScriptWithVariables("pw.expect(Symbol('test')).toBeLevel5xx()")
|
||||
}).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe("toHaveLength()", () => {
|
||||
test("test for strings", () => {
|
||||
expect(getTestResult("pw.expect('word').toHaveLength(4)", 0)).toEqual(
|
||||
"PASS"
|
||||
)
|
||||
expect(getTestResult("pw.expect('word').toHaveLength(5)", 0)).toEqual(
|
||||
"FAIL"
|
||||
)
|
||||
expect(getTestResult("pw.expect('word').not.toHaveLength(4)", 0)).toEqual(
|
||||
"FAIL"
|
||||
)
|
||||
expect(getTestResult("pw.expect('word').not.toHaveLength(5)", 0)).toEqual(
|
||||
"PASS"
|
||||
)
|
||||
})
|
||||
test("test for arrays", () => {
|
||||
const fruits =
|
||||
"['apples', 'bananas', 'oranges', 'grapes', 'strawberries', 'cherries']"
|
||||
expect(getTestResult(`pw.expect(${fruits}).toHaveLength(6)`, 0)).toEqual(
|
||||
"PASS"
|
||||
)
|
||||
expect(getTestResult(`pw.expect(${fruits}).toHaveLength(7)`, 0)).toEqual(
|
||||
"FAIL"
|
||||
)
|
||||
expect(
|
||||
getTestResult(`pw.expect(${fruits}).not.toHaveLength(6)`, 0)
|
||||
).toEqual("FAIL")
|
||||
expect(
|
||||
getTestResult(`pw.expect(${fruits}).not.toHaveLength(7)`, 0)
|
||||
).toEqual("PASS")
|
||||
})
|
||||
})
|
||||
|
||||
describe("toBeType()", () => {
|
||||
test("test for positive assertion", () => {
|
||||
expect(getTestResult("pw.expect('random').toBeType('string')", 0)).toEqual(
|
||||
"PASS"
|
||||
)
|
||||
expect(getTestResult("pw.expect(true).toBeType('boolean')", 0)).toEqual(
|
||||
"PASS"
|
||||
)
|
||||
expect(getTestResult("pw.expect(5).toBeType('number')", 0)).toEqual("PASS")
|
||||
expect(
|
||||
getTestResult("pw.expect(new Date()).toBeType('object')", 0)
|
||||
).toEqual("PASS")
|
||||
expect(
|
||||
getTestResult("pw.expect(undefined).toBeType('undefined')", 0)
|
||||
).toEqual("PASS")
|
||||
expect(
|
||||
getTestResult("pw.expect(BigInt(123)).toBeType('bigint')", 0)
|
||||
).toEqual("PASS")
|
||||
expect(
|
||||
getTestResult("pw.expect(Symbol('test')).toBeType('symbol')", 0)
|
||||
).toEqual("PASS")
|
||||
expect(
|
||||
getTestResult("pw.expect(function() {}).toBeType('function')", 0)
|
||||
).toEqual("PASS")
|
||||
})
|
||||
test("test for negative assertion", () => {
|
||||
expect(
|
||||
getTestResult("pw.expect('random').not.toBeType('string')", 0)
|
||||
).toEqual("FAIL")
|
||||
expect(getTestResult("pw.expect(true).not.toBeType('boolean')", 0)).toEqual(
|
||||
"FAIL"
|
||||
)
|
||||
expect(getTestResult("pw.expect(5).not.toBeType('number')", 0)).toEqual(
|
||||
"FAIL"
|
||||
)
|
||||
expect(
|
||||
getTestResult("pw.expect(new Date()).not.toBeType('object')", 0)
|
||||
).toEqual("FAIL")
|
||||
expect(
|
||||
getTestResult("pw.expect(undefined).not.toBeType('undefined')", 0)
|
||||
).toEqual("FAIL")
|
||||
expect(
|
||||
getTestResult("pw.expect(BigInt(123)).not.toBeType('bigint')", 0)
|
||||
).toEqual("FAIL")
|
||||
expect(
|
||||
getTestResult("pw.expect(Symbol('test')).not.toBeType('symbol')", 0)
|
||||
).toEqual("FAIL")
|
||||
expect(
|
||||
getTestResult("pw.expect(function() {}).not.toBeType('function')", 0)
|
||||
).toEqual("FAIL")
|
||||
})
|
||||
test("unexpected type", () => {
|
||||
expect(getTestResult("pw.expect('random').toBeType('unknown')", 0)).toEqual(
|
||||
"FAIL"
|
||||
)
|
||||
})
|
||||
})
|
||||
74
packages/hoppscotch-app/helpers/actions.ts
Normal file
74
packages/hoppscotch-app/helpers/actions.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
/* An `action` is a unique verb that is associated with certain thing that can be done on Hoppscotch.
|
||||
* For example, sending a request.
|
||||
*/
|
||||
|
||||
import { onBeforeUnmount, onMounted } from "@nuxtjs/composition-api"
|
||||
import { BehaviorSubject } from "rxjs"
|
||||
|
||||
export type HoppAction =
|
||||
| "request.send-cancel" // Send/Cancel a Hoppscotch Request
|
||||
| "request.reset" // Clear request data
|
||||
| "request.copy-link" // Copy Request Link
|
||||
| "request.save" // Save to Collections
|
||||
| "request.save-as" // Save As
|
||||
| "request.method.next" // Select Next Method
|
||||
| "request.method.prev" // Select Previous Method
|
||||
| "request.method.get" // Select GET Method
|
||||
| "request.method.head" // Select HEAD Method
|
||||
| "request.method.post" // Select POST Method
|
||||
| "request.method.put" // Select PUT Method
|
||||
| "request.method.delete" // Select DELETE Method
|
||||
| "flyouts.keybinds.toggle" // Shows the keybinds flyout
|
||||
| "modals.search.toggle" // Shows the search modal
|
||||
| "modals.support.toggle" // Shows the support modal
|
||||
| "modals.share.toggle" // Shows the share modal
|
||||
| "navigation.jump.rest" // Jump to REST page
|
||||
| "navigation.jump.graphql" // Jump to GraphQL page
|
||||
| "navigation.jump.realtime" // Jump to realtime page
|
||||
| "navigation.jump.documentation" // Jump to documentation page
|
||||
| "navigation.jump.settings" // Jump to settings page
|
||||
| "navigation.jump.back" // Jump to previous page
|
||||
| "navigation.jump.forward" // Jump to next page
|
||||
|
||||
type BoundActionList = {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
[_ in HoppAction]?: Array<() => void>
|
||||
}
|
||||
|
||||
const boundActions: BoundActionList = {}
|
||||
|
||||
export const activeActions$ = new BehaviorSubject<HoppAction[]>([])
|
||||
|
||||
export function bindAction(action: HoppAction, handler: () => void) {
|
||||
if (boundActions[action]) {
|
||||
boundActions[action]?.push(handler)
|
||||
} else {
|
||||
boundActions[action] = [handler]
|
||||
}
|
||||
|
||||
activeActions$.next(Object.keys(boundActions) as HoppAction[])
|
||||
}
|
||||
|
||||
export function invokeAction(action: HoppAction) {
|
||||
boundActions[action]?.forEach((handler) => handler())
|
||||
}
|
||||
|
||||
export function unbindAction(action: HoppAction, handler: () => void) {
|
||||
boundActions[action] = boundActions[action]?.filter((x) => x !== handler)
|
||||
|
||||
if (boundActions[action]?.length === 0) {
|
||||
delete boundActions[action]
|
||||
}
|
||||
|
||||
activeActions$.next(Object.keys(boundActions) as HoppAction[])
|
||||
}
|
||||
|
||||
export function defineActionHandler(action: HoppAction, handler: () => void) {
|
||||
onMounted(() => {
|
||||
bindAction(action, handler)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
unbindAction(action, handler)
|
||||
})
|
||||
}
|
||||
86
packages/hoppscotch-app/helpers/apollo.ts
Normal file
86
packages/hoppscotch-app/helpers/apollo.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import {
|
||||
ApolloClient,
|
||||
HttpLink,
|
||||
InMemoryCache,
|
||||
split,
|
||||
} from "@apollo/client/core"
|
||||
import { WebSocketLink } from "@apollo/client/link/ws"
|
||||
import { setContext } from "@apollo/client/link/context"
|
||||
import { getMainDefinition } from "@apollo/client/utilities"
|
||||
import { authIdToken$ } from "./fb/auth"
|
||||
|
||||
let authToken: String | null = null
|
||||
|
||||
export function registerApolloAuthUpdate() {
|
||||
authIdToken$.subscribe((token) => {
|
||||
authToken = token
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects auth token if available
|
||||
*/
|
||||
const authLink = setContext((_, { headers }) => {
|
||||
if (authToken) {
|
||||
return {
|
||||
headers: {
|
||||
...headers,
|
||||
authorization: `Bearer ${authToken}`,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
headers,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const httpLink = new HttpLink({
|
||||
uri:
|
||||
process.env.CONTEXT === "production"
|
||||
? "https://api.hoppscotch.io/graphql"
|
||||
: "https://api.hoppscotch.io/graphql",
|
||||
})
|
||||
|
||||
const wsLink = new WebSocketLink({
|
||||
uri:
|
||||
process.env.CONTEXT === "production"
|
||||
? "wss://api.hoppscotch.io/graphql"
|
||||
: "wss://api.hoppscotch.io/graphql",
|
||||
options: {
|
||||
reconnect: true,
|
||||
lazy: true,
|
||||
connectionParams: () => {
|
||||
return {
|
||||
authorization: `Bearer ${authToken}`,
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const splitLink = split(
|
||||
({ query }) => {
|
||||
const definition = getMainDefinition(query)
|
||||
return (
|
||||
definition.kind === "OperationDefinition" &&
|
||||
definition.operation === "subscription"
|
||||
)
|
||||
},
|
||||
wsLink,
|
||||
httpLink
|
||||
)
|
||||
|
||||
export const apolloClient = new ApolloClient({
|
||||
link: authLink.concat(splitLink),
|
||||
cache: new InMemoryCache(),
|
||||
defaultOptions: {
|
||||
query: {
|
||||
fetchPolicy: "network-only",
|
||||
errorPolicy: "ignore",
|
||||
},
|
||||
watchQuery: {
|
||||
fetchPolicy: "network-only",
|
||||
errorPolicy: "ignore",
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,91 @@
|
||||
import { codegens } from "../codegen"
|
||||
|
||||
const TEST_URL = "https://httpbin.org"
|
||||
const TEST_PATH_NAME = "/path/to"
|
||||
const TEST_QUERY_STRING = "?a=b"
|
||||
const TEST_HTTP_USER = "mockUser"
|
||||
const TEST_HTTP_PASSWORD = "mockPassword"
|
||||
const TEST_BEARER_TOKEN = "abcdefghijklmn"
|
||||
const TEST_RAW_REQUEST_BODY = "foo=bar&baz=qux"
|
||||
const TEST_RAW_PARAMS_JSON = '{"foo": "bar", "baz": "qux"}'
|
||||
const TEST_RAW_PARAMS_XML = `<?xml version='1.0' encoding='utf-8'?>
|
||||
<xml>
|
||||
<element foo="bar"></element>
|
||||
</xml>`
|
||||
const TEST_HEADERS = [
|
||||
{ key: "h1", value: "h1v" },
|
||||
{ key: "h2", value: "h2v" },
|
||||
]
|
||||
|
||||
codegens.forEach((codegen) => {
|
||||
describe(`generate request for ${codegen.name}`, () => {
|
||||
const testCases = [
|
||||
[
|
||||
"generate GET request",
|
||||
{
|
||||
url: TEST_URL,
|
||||
pathName: TEST_PATH_NAME,
|
||||
queryString: TEST_QUERY_STRING,
|
||||
auth: "Basic Auth",
|
||||
httpUser: TEST_HTTP_USER,
|
||||
httpPassword: TEST_HTTP_PASSWORD,
|
||||
method: "GET",
|
||||
rawInput: false,
|
||||
rawParams: "",
|
||||
rawRequestBody: "",
|
||||
headers: TEST_HEADERS,
|
||||
},
|
||||
],
|
||||
[
|
||||
"generate POST request for JSON",
|
||||
{
|
||||
url: TEST_URL,
|
||||
pathName: TEST_PATH_NAME,
|
||||
queryString: TEST_QUERY_STRING,
|
||||
auth: "Bearer Token",
|
||||
bearerToken: TEST_BEARER_TOKEN,
|
||||
method: "POST",
|
||||
rawInput: true,
|
||||
rawParams: TEST_RAW_PARAMS_JSON,
|
||||
rawRequestBody: "",
|
||||
contentType: "application/json",
|
||||
headers: TEST_HEADERS,
|
||||
},
|
||||
],
|
||||
[
|
||||
"generate POST request for XML",
|
||||
{
|
||||
url: TEST_URL,
|
||||
pathName: TEST_PATH_NAME,
|
||||
queryString: TEST_QUERY_STRING,
|
||||
auth: "OAuth 2.0",
|
||||
bearerToken: TEST_BEARER_TOKEN,
|
||||
method: "POST",
|
||||
rawInput: true,
|
||||
rawParams: TEST_RAW_PARAMS_XML,
|
||||
rawRequestBody: "",
|
||||
contentType: "application/xml",
|
||||
headers: TEST_HEADERS,
|
||||
},
|
||||
],
|
||||
[
|
||||
"generate PUT request for www-form-urlencoded",
|
||||
{
|
||||
url: TEST_URL,
|
||||
pathName: TEST_PATH_NAME,
|
||||
queryString: TEST_QUERY_STRING,
|
||||
method: "PUT",
|
||||
rawInput: false,
|
||||
rawRequestBody: TEST_RAW_REQUEST_BODY,
|
||||
contentType: "application/x-www-form-urlencoded",
|
||||
headers: [],
|
||||
},
|
||||
],
|
||||
]
|
||||
|
||||
test.each(testCases)("%s", (_, context) => {
|
||||
const result = codegen.generator(context)
|
||||
expect(result).toMatchSnapshot()
|
||||
})
|
||||
})
|
||||
})
|
||||
199
packages/hoppscotch-app/helpers/codegen/codegen.ts
Normal file
199
packages/hoppscotch-app/helpers/codegen/codegen.ts
Normal file
@@ -0,0 +1,199 @@
|
||||
import {
|
||||
FormDataKeyValue,
|
||||
HoppRESTHeader,
|
||||
HoppRESTParam,
|
||||
} from "../types/HoppRESTRequest"
|
||||
import { EffectiveHoppRESTRequest } from "../utils/EffectiveURL"
|
||||
import { CLibcurlCodegen } from "./generators/c-libcurl"
|
||||
import { CsRestsharpCodegen } from "./generators/cs-restsharp"
|
||||
import { CurlCodegen } from "./generators/curl"
|
||||
import { GoNativeCodegen } from "./generators/go-native"
|
||||
import { JavaOkhttpCodegen } from "./generators/java-okhttp"
|
||||
import { JavaUnirestCodegen } from "./generators/java-unirest"
|
||||
import { JavascriptFetchCodegen } from "./generators/javascript-fetch"
|
||||
import { JavascriptJqueryCodegen } from "./generators/javascript-jquery"
|
||||
import { JavascriptXhrCodegen } from "./generators/javascript-xhr"
|
||||
import { NodejsAxiosCodegen } from "./generators/nodejs-axios"
|
||||
import { NodejsNativeCodegen } from "./generators/nodejs-native"
|
||||
import { NodejsRequestCodegen } from "./generators/nodejs-request"
|
||||
import { NodejsUnirestCodegen } from "./generators/nodejs-unirest"
|
||||
import { PhpCurlCodegen } from "./generators/php-curl"
|
||||
import { PowershellRestmethodCodegen } from "./generators/powershell-restmethod"
|
||||
import { PythonHttpClientCodegen } from "./generators/python-http-client"
|
||||
import { PythonRequestsCodegen } from "./generators/python-requests"
|
||||
import { RubyNetHttpCodeGen } from "./generators/ruby-net-http"
|
||||
import { SalesforceApexCodegen } from "./generators/salesforce-apex"
|
||||
import { ShellHttpieCodegen } from "./generators/shell-httpie"
|
||||
import { ShellWgetCodegen } from "./generators/shell-wget"
|
||||
|
||||
/* Register code generators here.
|
||||
* A code generator is defined as an object with the following structure.
|
||||
*
|
||||
* id: string
|
||||
* name: string
|
||||
* language: string // a string identifier used in ace editor for syntax highlighting
|
||||
* // see node_modules/ace-builds/src-noconflict/mode-** files for valid value
|
||||
* generator: (ctx) => string
|
||||
*
|
||||
*/
|
||||
export const codegens = [
|
||||
CLibcurlCodegen,
|
||||
CsRestsharpCodegen,
|
||||
CurlCodegen,
|
||||
GoNativeCodegen,
|
||||
JavaOkhttpCodegen,
|
||||
JavaUnirestCodegen,
|
||||
JavascriptFetchCodegen,
|
||||
JavascriptJqueryCodegen,
|
||||
JavascriptXhrCodegen,
|
||||
NodejsAxiosCodegen,
|
||||
NodejsNativeCodegen,
|
||||
NodejsRequestCodegen,
|
||||
NodejsUnirestCodegen,
|
||||
PhpCurlCodegen,
|
||||
PowershellRestmethodCodegen,
|
||||
PythonHttpClientCodegen,
|
||||
PythonRequestsCodegen,
|
||||
RubyNetHttpCodeGen,
|
||||
SalesforceApexCodegen,
|
||||
ShellHttpieCodegen,
|
||||
ShellWgetCodegen,
|
||||
]
|
||||
|
||||
export type HoppCodegenContext = {
|
||||
name: string
|
||||
method: string
|
||||
uri: string
|
||||
url: string
|
||||
pathName: string
|
||||
auth: any // TODO: Change this
|
||||
httpUser: string | null
|
||||
httpPassword: string | null
|
||||
bearerToken: string | null
|
||||
headers: HoppRESTHeader[]
|
||||
params: HoppRESTParam[]
|
||||
bodyParams: FormDataKeyValue[]
|
||||
rawParams: string | null
|
||||
rawInput: boolean
|
||||
rawRequestBody: string | null
|
||||
contentType: string | null
|
||||
queryString: string
|
||||
}
|
||||
|
||||
export function generateCodeWithGenerator(
|
||||
codegenID: string,
|
||||
context: HoppCodegenContext
|
||||
) {
|
||||
if (codegenID) {
|
||||
const gen = codegens.find(({ id }) => id === codegenID)
|
||||
return gen ? gen.generator(context) : ""
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
function getCodegenAuth(
|
||||
request: EffectiveHoppRESTRequest
|
||||
): Pick<
|
||||
HoppCodegenContext,
|
||||
"auth" | "bearerToken" | "httpUser" | "httpPassword"
|
||||
> {
|
||||
if (!request.auth.authActive || request.auth.authType === "none") {
|
||||
return {
|
||||
auth: "None",
|
||||
httpUser: null,
|
||||
httpPassword: null,
|
||||
bearerToken: null,
|
||||
}
|
||||
}
|
||||
|
||||
if (request.auth.authType === "basic") {
|
||||
return {
|
||||
auth: "Basic Auth",
|
||||
httpUser: request.auth.username,
|
||||
httpPassword: request.auth.password,
|
||||
bearerToken: null,
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
auth: "Bearer Token",
|
||||
httpUser: null,
|
||||
httpPassword: null,
|
||||
bearerToken: request.auth.token,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getCodegenGeneralRESTInfo(
|
||||
request: EffectiveHoppRESTRequest
|
||||
): Pick<
|
||||
HoppCodegenContext,
|
||||
| "name"
|
||||
| "uri"
|
||||
| "url"
|
||||
| "method"
|
||||
| "queryString"
|
||||
| "pathName"
|
||||
| "params"
|
||||
| "headers"
|
||||
> {
|
||||
const urlObj = new URL(request.effectiveFinalURL)
|
||||
request.effectiveFinalParams.forEach(({ key, value }) => {
|
||||
urlObj.searchParams.append(key, value)
|
||||
})
|
||||
|
||||
// Remove authorization headers if auth is specified (because see #1798)
|
||||
const finalHeaders =
|
||||
request.auth.authActive && request.auth.authType !== "none"
|
||||
? request.effectiveFinalHeaders
|
||||
.filter((x) => x.key.toLowerCase() !== "authorization")
|
||||
.map((x) => ({ ...x, active: true }))
|
||||
: request.effectiveFinalHeaders.map((x) => ({ ...x, active: true }))
|
||||
|
||||
console.log(finalHeaders)
|
||||
|
||||
return {
|
||||
name: request.name,
|
||||
uri: request.effectiveFinalURL,
|
||||
headers: finalHeaders,
|
||||
params: request.effectiveFinalParams.map((x) => ({ ...x, active: true })),
|
||||
method: request.method,
|
||||
url: urlObj.origin,
|
||||
queryString: urlObj.searchParams.toString(),
|
||||
pathName: urlObj.pathname,
|
||||
}
|
||||
}
|
||||
|
||||
function getCodegenReqBodyData(
|
||||
request: EffectiveHoppRESTRequest
|
||||
): Pick<
|
||||
HoppCodegenContext,
|
||||
"rawRequestBody" | "rawInput" | "contentType" | "bodyParams" | "rawParams"
|
||||
> {
|
||||
return {
|
||||
contentType: request.body.contentType,
|
||||
rawInput: request.body.contentType !== "multipart/form-data",
|
||||
rawRequestBody:
|
||||
request.body.contentType !== "multipart/form-data"
|
||||
? request.body.body
|
||||
: null,
|
||||
bodyParams:
|
||||
request.body.contentType === "multipart/form-data"
|
||||
? request.body.body
|
||||
: [],
|
||||
rawParams:
|
||||
request.body.contentType !== "multipart/form-data"
|
||||
? request.body.body
|
||||
: null,
|
||||
}
|
||||
}
|
||||
|
||||
export function generateCodegenContext(
|
||||
request: EffectiveHoppRESTRequest
|
||||
): HoppCodegenContext {
|
||||
return {
|
||||
...getCodegenAuth(request),
|
||||
...getCodegenGeneralRESTInfo(request),
|
||||
...getCodegenReqBodyData(request),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
export const CLibcurlCodegen = {
|
||||
id: "c-libcurl",
|
||||
name: "C libcurl",
|
||||
language: "c_cpp",
|
||||
generator: ({
|
||||
auth,
|
||||
httpUser,
|
||||
httpPassword,
|
||||
method,
|
||||
url,
|
||||
pathName,
|
||||
queryString,
|
||||
bearerToken,
|
||||
headers,
|
||||
rawInput,
|
||||
rawParams,
|
||||
rawRequestBody,
|
||||
contentType,
|
||||
}) => {
|
||||
const requestString = []
|
||||
|
||||
requestString.push("CURL *hnd = curl_easy_init();")
|
||||
requestString.push(
|
||||
`curl_easy_setopt(hnd, CURLOPT_CUSTOMREQUEST, "${method}");`
|
||||
)
|
||||
requestString.push(
|
||||
`curl_easy_setopt(hnd, CURLOPT_URL, "${url}${pathName}${queryString}");`
|
||||
)
|
||||
requestString.push(`struct curl_slist *headers = NULL;`)
|
||||
|
||||
if (headers) {
|
||||
headers.forEach(({ key, value }) => {
|
||||
if (key)
|
||||
requestString.push(
|
||||
`headers = curl_slist_append(headers, "${key}: ${value}");`
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
if (auth === "Basic Auth") {
|
||||
const basic = `${httpUser}:${httpPassword}`
|
||||
requestString.push(
|
||||
`headers = curl_slist_append(headers, "Authorization: Basic ${window.btoa(
|
||||
unescape(encodeURIComponent(basic))
|
||||
)}");`
|
||||
)
|
||||
} else if (auth === "Bearer Token" || auth === "OAuth 2.0") {
|
||||
requestString.push(
|
||||
`headers = curl_slist_append(headers, "Authorization: Bearer ${bearerToken}");`
|
||||
)
|
||||
}
|
||||
|
||||
if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
|
||||
let requestBody = rawInput ? rawParams : rawRequestBody
|
||||
|
||||
if (contentType.includes("x-www-form-urlencoded")) {
|
||||
requestBody = `"${requestBody}"`
|
||||
} else requestBody = JSON.stringify(requestBody)
|
||||
|
||||
requestString.push(
|
||||
`headers = curl_slist_append(headers, "Content-Type: ${contentType}");`
|
||||
)
|
||||
requestString.push("curl_easy_setopt(hnd, CURLOPT_HTTPHEADER, headers);")
|
||||
requestString.push(
|
||||
`curl_easy_setopt(hnd, CURLOPT_POSTFIELDS, ${requestBody});`
|
||||
)
|
||||
} else
|
||||
requestString.push("curl_easy_setopt(hnd, CURLOPT_HTTPHEADER, headers);")
|
||||
|
||||
requestString.push(`CURLcode ret = curl_easy_perform(hnd);`)
|
||||
return requestString.join("\n")
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import { isJSONContentType } from "~/helpers/utils/contenttypes"
|
||||
|
||||
export const CsRestsharpCodegen = {
|
||||
id: "cs-restsharp",
|
||||
name: "C# RestSharp",
|
||||
language: "csharp",
|
||||
generator: ({
|
||||
url,
|
||||
pathName,
|
||||
queryString,
|
||||
auth,
|
||||
httpUser,
|
||||
httpPassword,
|
||||
bearerToken,
|
||||
method,
|
||||
rawInput,
|
||||
rawParams,
|
||||
rawRequestBody,
|
||||
contentType,
|
||||
headers,
|
||||
}) => {
|
||||
const requestString = []
|
||||
|
||||
// initial request setup
|
||||
let requestBody = rawInput ? rawParams : rawRequestBody
|
||||
requestBody = requestBody.replace(/"/g, '""') // escape quotes for C# verbatim string compatibility
|
||||
|
||||
// prepare data
|
||||
let requestDataFormat
|
||||
let requestContentType
|
||||
|
||||
if (isJSONContentType(contentType)) {
|
||||
requestDataFormat = "DataFormat.Json"
|
||||
requestContentType = "text/json"
|
||||
} else {
|
||||
requestDataFormat = "DataFormat.Xml"
|
||||
requestContentType = "text/xml"
|
||||
}
|
||||
|
||||
const verbs = [
|
||||
{ verb: "GET", csMethod: "Get" },
|
||||
{ verb: "POST", csMethod: "Post" },
|
||||
{ verb: "PUT", csMethod: "Put" },
|
||||
{ verb: "PATCH", csMethod: "Patch" },
|
||||
{ verb: "DELETE", csMethod: "Delete" },
|
||||
]
|
||||
|
||||
// create client and request
|
||||
requestString.push(`var client = new RestClient("${url}");\n\n`)
|
||||
requestString.push(
|
||||
`var request = new RestRequest("${pathName}${queryString}", ${requestDataFormat});\n\n`
|
||||
)
|
||||
|
||||
// authentification
|
||||
if (auth === "Basic Auth") {
|
||||
requestString.push(
|
||||
`client.Authenticator = new HttpBasicAuthenticator("${httpUser}", "${httpPassword}");\n`
|
||||
)
|
||||
} else if (auth === "Bearer Token" || auth === "OAuth 2.0") {
|
||||
requestString.push(
|
||||
`request.AddHeader("Authorization", "Bearer ${bearerToken}");\n`
|
||||
)
|
||||
}
|
||||
|
||||
// content type
|
||||
if (contentType) {
|
||||
requestString.push(
|
||||
`request.AddHeader("Content-Type", "${contentType}");\n`
|
||||
)
|
||||
}
|
||||
|
||||
// custom headers
|
||||
if (headers) {
|
||||
headers.forEach(({ key, value }) => {
|
||||
if (key) {
|
||||
requestString.push(`request.AddHeader("${key}", "${value}");\n`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
requestString.push(`\n`)
|
||||
|
||||
// set body
|
||||
if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
|
||||
requestString.push(
|
||||
`request.AddParameter("${requestContentType}", @"${requestBody}", ParameterType.RequestBody);\n\n`
|
||||
)
|
||||
}
|
||||
|
||||
// process
|
||||
const verb = verbs.find((v) => v.verb === method)
|
||||
requestString.push(`var response = client.${verb.csMethod}(request);\n\n`)
|
||||
|
||||
// analyse result
|
||||
requestString.push(
|
||||
`if (!response.IsSuccessful)\n{\n Console.WriteLine("An error occurred " + response.ErrorMessage);\n}\n\n`
|
||||
)
|
||||
|
||||
requestString.push(`var result = response.Content;\n`)
|
||||
|
||||
return requestString.join("")
|
||||
},
|
||||
}
|
||||
45
packages/hoppscotch-app/helpers/codegen/generators/curl.js
Normal file
45
packages/hoppscotch-app/helpers/codegen/generators/curl.js
Normal file
@@ -0,0 +1,45 @@
|
||||
export const CurlCodegen = {
|
||||
id: "curl",
|
||||
name: "cURL",
|
||||
language: "sh",
|
||||
generator: ({
|
||||
url,
|
||||
pathName,
|
||||
queryString,
|
||||
auth,
|
||||
httpUser,
|
||||
httpPassword,
|
||||
bearerToken,
|
||||
method,
|
||||
rawInput,
|
||||
rawParams,
|
||||
rawRequestBody,
|
||||
contentType,
|
||||
headers,
|
||||
}) => {
|
||||
const requestString = []
|
||||
requestString.push(`curl -X ${method}`)
|
||||
requestString.push(` '${url}${pathName}${queryString}'`)
|
||||
if (auth === "Basic Auth") {
|
||||
const basic = `${httpUser}:${httpPassword}`
|
||||
requestString.push(
|
||||
` -H 'Authorization: Basic ${window.btoa(
|
||||
unescape(encodeURIComponent(basic))
|
||||
)}'`
|
||||
)
|
||||
} else if (auth === "Bearer Token" || auth === "OAuth 2.0") {
|
||||
requestString.push(` -H 'Authorization: Bearer ${bearerToken}'`)
|
||||
}
|
||||
if (headers) {
|
||||
headers.forEach(({ key, value }) => {
|
||||
if (key) requestString.push(` -H '${key}: ${value}'`)
|
||||
})
|
||||
}
|
||||
if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
|
||||
const requestBody = rawInput ? rawParams : rawRequestBody
|
||||
requestString.push(` -H 'Content-Type: ${contentType}; charset=utf-8'`)
|
||||
requestString.push(` -d '${requestBody}'`)
|
||||
}
|
||||
return requestString.join(" \\\n")
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { isJSONContentType } from "~/helpers/utils/contenttypes"
|
||||
|
||||
export const GoNativeCodegen = {
|
||||
id: "go-native",
|
||||
name: "Go Native",
|
||||
language: "golang",
|
||||
generator: ({
|
||||
url,
|
||||
pathName,
|
||||
queryString,
|
||||
auth,
|
||||
httpUser,
|
||||
httpPassword,
|
||||
bearerToken,
|
||||
method,
|
||||
rawInput,
|
||||
rawParams,
|
||||
rawRequestBody,
|
||||
contentType,
|
||||
headers,
|
||||
}) => {
|
||||
const requestString = []
|
||||
let genHeaders = []
|
||||
// initial request setup
|
||||
const requestBody = rawInput ? rawParams : rawRequestBody
|
||||
if (method === "GET") {
|
||||
requestString.push(
|
||||
`req, err := http.NewRequest("${method}", "${url}${pathName}${queryString}")\n`
|
||||
)
|
||||
}
|
||||
if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
|
||||
genHeaders.push(`req.Header.Set("Content-Type", "${contentType}")\n`)
|
||||
if (isJSONContentType(contentType)) {
|
||||
requestString.push(`var reqBody = []byte(\`${requestBody}\`)\n\n`)
|
||||
requestString.push(
|
||||
`req, err := http.NewRequest("${method}", "${url}${pathName}${queryString}", bytes.NewBuffer(reqBody))\n`
|
||||
)
|
||||
} else if (contentType.includes("x-www-form-urlencoded")) {
|
||||
requestString.push(
|
||||
`req, err := http.NewRequest("${method}", "${url}${pathName}${queryString}", strings.NewReader("${requestBody}"))\n`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// headers
|
||||
// auth
|
||||
if (auth === "Basic Auth") {
|
||||
const basic = `${httpUser}:${httpPassword}`
|
||||
genHeaders.push(
|
||||
`req.Header.Set("Authorization", "Basic ${window.btoa(
|
||||
unescape(encodeURIComponent(basic))
|
||||
)}")\n`
|
||||
)
|
||||
} else if (auth === "Bearer Token" || auth === "OAuth 2.0") {
|
||||
genHeaders.push(
|
||||
`req.Header.Set("Authorization", "Bearer ${bearerToken}")\n`
|
||||
)
|
||||
}
|
||||
// custom headers
|
||||
if (headers) {
|
||||
headers.forEach(({ key, value }) => {
|
||||
if (key) genHeaders.push(`req.Header.Set("${key}", "${value}")\n`)
|
||||
})
|
||||
}
|
||||
genHeaders = genHeaders.join("").slice(0, -1)
|
||||
requestString.push(`${genHeaders}\n`)
|
||||
requestString.push(
|
||||
`if err != nil {\n log.Fatalf("An error occurred %v", err)\n}\n\n`
|
||||
)
|
||||
|
||||
// request boilerplate
|
||||
requestString.push(`client := &http.Client{}\n`)
|
||||
requestString.push(
|
||||
`resp, err := client.Do(req)\nif err != nil {\n log.Fatalf("An error occurred %v", err)\n}\n\n`
|
||||
)
|
||||
requestString.push(`defer resp.Body.Close()\n`)
|
||||
requestString.push(
|
||||
`body, err := ioutil.ReadAll(resp.Body)\nif err != nil {\n log.Fatalln(err)\n}\n`
|
||||
)
|
||||
return requestString.join("")
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
export const JavaOkhttpCodegen = {
|
||||
id: "java-okhttp",
|
||||
name: "Java OkHttp",
|
||||
language: "java",
|
||||
generator: ({
|
||||
auth,
|
||||
httpUser,
|
||||
httpPassword,
|
||||
method,
|
||||
url,
|
||||
pathName,
|
||||
queryString,
|
||||
bearerToken,
|
||||
headers,
|
||||
rawInput,
|
||||
rawParams,
|
||||
rawRequestBody,
|
||||
contentType,
|
||||
}) => {
|
||||
const requestString = []
|
||||
|
||||
requestString.push(
|
||||
"OkHttpClient client = new OkHttpClient().newBuilder().build();"
|
||||
)
|
||||
|
||||
if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
|
||||
let requestBody = rawInput ? rawParams : rawRequestBody
|
||||
|
||||
if (contentType.includes("x-www-form-urlencoded")) {
|
||||
requestBody = `"${requestBody}"`
|
||||
} else requestBody = JSON.stringify(requestBody)
|
||||
|
||||
requestString.push(
|
||||
`MediaType mediaType = MediaType.parse("${contentType}");`
|
||||
)
|
||||
requestString.push(
|
||||
`RequestBody body = RequestBody.create(mediaType,${requestBody});`
|
||||
)
|
||||
}
|
||||
|
||||
requestString.push("Request request = new Request.Builder()")
|
||||
requestString.push(`.url("${url}${pathName}${queryString}")`)
|
||||
|
||||
if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
|
||||
requestString.push(`.method("${method}", body)`)
|
||||
} else {
|
||||
requestString.push(`.method("${method}", null)`)
|
||||
}
|
||||
|
||||
if (auth === "Basic Auth") {
|
||||
const basic = `${httpUser}:${httpPassword}`
|
||||
requestString.push(
|
||||
`.addHeader("authorization", "Basic ${window.btoa(
|
||||
unescape(encodeURIComponent(basic))
|
||||
)}") \n`
|
||||
)
|
||||
} else if (auth === "Bearer Token" || auth === "OAuth 2.0") {
|
||||
requestString.push(
|
||||
`.addHeader("authorization", "Bearer ${bearerToken}" ) \n`
|
||||
)
|
||||
}
|
||||
|
||||
if (headers) {
|
||||
headers.forEach(({ key, value }) => {
|
||||
if (key) requestString.push(`.addHeader("${key}", "${value}")`)
|
||||
})
|
||||
}
|
||||
|
||||
requestString.push(`.build();`)
|
||||
requestString.push("Response response = client.newCall(request).execute();")
|
||||
return requestString.join("\n")
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
export const JavaUnirestCodegen = {
|
||||
id: "java-unirest",
|
||||
name: "Java Unirest",
|
||||
language: "java",
|
||||
generator: ({
|
||||
url,
|
||||
pathName,
|
||||
queryString,
|
||||
auth,
|
||||
httpUser,
|
||||
httpPassword,
|
||||
bearerToken,
|
||||
method,
|
||||
rawInput,
|
||||
rawParams,
|
||||
rawRequestBody,
|
||||
contentType,
|
||||
headers,
|
||||
}) => {
|
||||
const requestString = []
|
||||
// initial request setup
|
||||
let requestBody = rawInput ? rawParams : rawRequestBody
|
||||
const verbs = [
|
||||
{ verb: "GET", unirestMethod: "get" },
|
||||
{ verb: "POST", unirestMethod: "post" },
|
||||
{ verb: "PUT", unirestMethod: "put" },
|
||||
{ verb: "PATCH", unirestMethod: "patch" },
|
||||
{ verb: "DELETE", unirestMethod: "delete" },
|
||||
{ verb: "HEAD", unirestMethod: "head" },
|
||||
{ verb: "OPTIONS", unirestMethod: "options" },
|
||||
]
|
||||
// create client and request
|
||||
const verb = verbs.find((v) => v.verb === method)
|
||||
requestString.push(
|
||||
`HttpResponse<String> response = Unirest.${verb.unirestMethod}("${url}${pathName}${queryString}")\n`
|
||||
)
|
||||
if (auth === "Basic Auth") {
|
||||
const basic = `${httpUser}:${httpPassword}`
|
||||
requestString.push(
|
||||
`.header("authorization", "Basic ${window.btoa(
|
||||
unescape(encodeURIComponent(basic))
|
||||
)}") \n`
|
||||
)
|
||||
} else if (auth === "Bearer Token" || auth === "OAuth 2.0") {
|
||||
requestString.push(`.header("authorization", "Bearer ${bearerToken}") \n`)
|
||||
}
|
||||
// custom headers
|
||||
if (headers) {
|
||||
headers.forEach(({ key, value }) => {
|
||||
if (key) {
|
||||
requestString.push(`.header("${key}", "${value}")\n`)
|
||||
}
|
||||
})
|
||||
}
|
||||
if (contentType) {
|
||||
requestString.push(`.header("Content-Type", "${contentType}")\n`)
|
||||
}
|
||||
|
||||
// set body
|
||||
if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
|
||||
if (contentType.includes("x-www-form-urlencoded")) {
|
||||
requestBody = `"${requestBody}"`
|
||||
} else {
|
||||
requestBody = JSON.stringify(requestBody)
|
||||
}
|
||||
|
||||
requestString.push(`.body(${requestBody})`)
|
||||
}
|
||||
requestString.push(`\n.asString();\n`)
|
||||
return requestString.join("")
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { isJSONContentType } from "~/helpers/utils/contenttypes"
|
||||
|
||||
export const JavascriptFetchCodegen = {
|
||||
id: "js-fetch",
|
||||
name: "JavaScript Fetch",
|
||||
language: "javascript",
|
||||
generator: ({
|
||||
url,
|
||||
pathName,
|
||||
queryString,
|
||||
auth,
|
||||
httpUser,
|
||||
httpPassword,
|
||||
bearerToken,
|
||||
method,
|
||||
rawInput,
|
||||
rawParams,
|
||||
rawRequestBody,
|
||||
contentType,
|
||||
headers,
|
||||
}) => {
|
||||
const requestString = []
|
||||
let genHeaders = []
|
||||
requestString.push(`fetch("${url}${pathName}${queryString}", {\n`)
|
||||
requestString.push(` method: "${method}",\n`)
|
||||
if (auth === "Basic Auth") {
|
||||
const basic = `${httpUser}:${httpPassword}`
|
||||
genHeaders.push(
|
||||
` "Authorization": "Basic ${window.btoa(
|
||||
unescape(encodeURIComponent(basic))
|
||||
)}",\n`
|
||||
)
|
||||
} else if (auth === "Bearer Token" || auth === "OAuth 2.0") {
|
||||
genHeaders.push(` "Authorization": "Bearer ${bearerToken}",\n`)
|
||||
}
|
||||
if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
|
||||
let requestBody = rawInput ? rawParams : rawRequestBody
|
||||
if (isJSONContentType(contentType)) {
|
||||
requestBody = `JSON.stringify(${requestBody})`
|
||||
} else if (contentType.includes("x-www-form-urlencoded")) {
|
||||
requestBody = `"${requestBody}"`
|
||||
}
|
||||
|
||||
requestString.push(` body: ${requestBody},\n`)
|
||||
genHeaders.push(` "Content-Type": "${contentType}; charset=utf-8",\n`)
|
||||
}
|
||||
if (headers) {
|
||||
headers.forEach(({ key, value }) => {
|
||||
if (key) genHeaders.push(` "${key}": "${value}",\n`)
|
||||
})
|
||||
}
|
||||
genHeaders = genHeaders.join("").slice(0, -2)
|
||||
requestString.push(` headers: {\n${genHeaders}\n },\n`)
|
||||
requestString.push(' credentials: "same-origin"\n')
|
||||
requestString.push("}).then(function(response) {\n")
|
||||
requestString.push(" response.status\n")
|
||||
requestString.push(" response.statusText\n")
|
||||
requestString.push(" response.headers\n")
|
||||
requestString.push(" response.url\n\n")
|
||||
requestString.push(" return response.text()\n")
|
||||
requestString.push("}).catch(function(e) {\n")
|
||||
requestString.push(" console.error(e)\n")
|
||||
requestString.push("})")
|
||||
return requestString.join("")
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
export const JavascriptJqueryCodegen = {
|
||||
id: "js-jquery",
|
||||
name: "JavaScript jQuery",
|
||||
language: "javascript",
|
||||
generator: ({
|
||||
url,
|
||||
pathName,
|
||||
queryString,
|
||||
auth,
|
||||
httpUser,
|
||||
httpPassword,
|
||||
bearerToken,
|
||||
method,
|
||||
rawInput,
|
||||
rawParams,
|
||||
rawRequestBody,
|
||||
contentType,
|
||||
headers,
|
||||
}) => {
|
||||
const requestString = []
|
||||
const genHeaders = []
|
||||
|
||||
requestString.push(
|
||||
`jQuery.ajax({\n url: "${url}${pathName}${queryString}"`
|
||||
)
|
||||
requestString.push(`,\n method: "${method.toUpperCase()}"`)
|
||||
const requestBody = rawInput ? rawParams : rawRequestBody
|
||||
|
||||
if (requestBody.length !== 0) {
|
||||
requestString.push(`,\n body: ${requestBody}`)
|
||||
}
|
||||
if (headers) {
|
||||
headers.forEach(({ key, value }) => {
|
||||
if (key) genHeaders.push(` "${key}": "${value}",\n`)
|
||||
})
|
||||
}
|
||||
|
||||
if (contentType) {
|
||||
genHeaders.push(` "Content-Type": "${contentType}; charset=utf-8",\n`)
|
||||
requestString.push(`,\n contentType: "${contentType}; charset=utf-8"`)
|
||||
}
|
||||
|
||||
if (auth === "Basic Auth") {
|
||||
const basic = `${httpUser}:${httpPassword}`
|
||||
genHeaders.push(
|
||||
` "Authorization": "Basic ${window.btoa(
|
||||
unescape(encodeURIComponent(basic))
|
||||
)}",\n`
|
||||
)
|
||||
} else if (auth === "Bearer Token" || auth === "OAuth 2.0") {
|
||||
genHeaders.push(` "Authorization": "Bearer ${bearerToken}",\n`)
|
||||
}
|
||||
requestString.push(
|
||||
`,\n headers: {\n${genHeaders.join("").slice(0, -2)}\n }\n})`
|
||||
)
|
||||
requestString.push(".then(response => {\n")
|
||||
requestString.push(" console.log(response);\n")
|
||||
requestString.push("})")
|
||||
requestString.push(".catch(e => {\n")
|
||||
requestString.push(" console.error(e);\n")
|
||||
requestString.push("})\n")
|
||||
return requestString.join("")
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { isJSONContentType } from "~/helpers/utils/contenttypes"
|
||||
|
||||
export const JavascriptXhrCodegen = {
|
||||
id: "js-xhr",
|
||||
name: "JavaScript XHR",
|
||||
language: "javascript",
|
||||
generator: ({
|
||||
auth,
|
||||
httpUser,
|
||||
httpPassword,
|
||||
method,
|
||||
url,
|
||||
pathName,
|
||||
queryString,
|
||||
bearerToken,
|
||||
headers,
|
||||
rawInput,
|
||||
rawParams,
|
||||
rawRequestBody,
|
||||
contentType,
|
||||
}) => {
|
||||
const requestString = []
|
||||
requestString.push("const xhr = new XMLHttpRequest()")
|
||||
|
||||
const user = auth === "Basic Auth" ? `'${httpUser}'` : null
|
||||
const password = auth === "Basic Auth" ? `'${httpPassword}'` : null
|
||||
requestString.push(
|
||||
`xhr.open('${method}', '${url}${pathName}${queryString}', true, ${user}, ${password})`
|
||||
)
|
||||
if (auth === "Bearer Token" || auth === "OAuth 2.0") {
|
||||
requestString.push(
|
||||
`xhr.setRequestHeader('Authorization', 'Bearer ${bearerToken}')`
|
||||
)
|
||||
}
|
||||
if (headers) {
|
||||
headers.forEach(({ key, value }) => {
|
||||
if (key)
|
||||
requestString.push(`xhr.setRequestHeader('${key}', '${value}')`)
|
||||
})
|
||||
}
|
||||
if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
|
||||
let requestBody = rawInput ? rawParams : rawRequestBody
|
||||
if (isJSONContentType(contentType)) {
|
||||
requestBody = `JSON.stringify(${requestBody})`
|
||||
} else if (contentType.includes("x-www-form-urlencoded")) {
|
||||
requestBody = `"${requestBody}"`
|
||||
}
|
||||
requestString.push(
|
||||
`xhr.setRequestHeader('Content-Type', '${contentType}; charset=utf-8')`
|
||||
)
|
||||
requestString.push(`xhr.send(${requestBody})`)
|
||||
} else {
|
||||
requestString.push("xhr.send()")
|
||||
}
|
||||
return requestString.join("\n")
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
export const NodejsAxiosCodegen = {
|
||||
id: "nodejs-axios",
|
||||
name: "NodeJs Axios",
|
||||
language: "javascript",
|
||||
generator: ({
|
||||
url,
|
||||
pathName,
|
||||
queryString,
|
||||
auth,
|
||||
httpUser,
|
||||
httpPassword,
|
||||
bearerToken,
|
||||
method,
|
||||
rawInput,
|
||||
rawParams,
|
||||
rawRequestBody,
|
||||
contentType,
|
||||
headers,
|
||||
}) => {
|
||||
const requestString = []
|
||||
const genHeaders = []
|
||||
const requestBody = rawInput ? rawParams : rawRequestBody
|
||||
|
||||
requestString.push(
|
||||
`axios.${method.toLowerCase()}('${url}${pathName}${queryString}'`
|
||||
)
|
||||
if (requestBody.length !== 0) {
|
||||
requestString.push(", ")
|
||||
}
|
||||
if (headers) {
|
||||
headers.forEach(({ key, value }) => {
|
||||
if (key) genHeaders.push(` "${key}": "${value}",\n`)
|
||||
})
|
||||
}
|
||||
if (contentType) {
|
||||
genHeaders.push(`"Content-Type": "${contentType}; charset=utf-8",\n`)
|
||||
}
|
||||
if (auth === "Basic Auth") {
|
||||
const basic = `${httpUser}:${httpPassword}`
|
||||
genHeaders.push(
|
||||
` "Authorization": "Basic ${window.btoa(
|
||||
unescape(encodeURIComponent(basic))
|
||||
)}",\n`
|
||||
)
|
||||
} else if (auth === "Bearer Token" || auth === "OAuth 2.0") {
|
||||
genHeaders.push(` "Authorization": "Bearer ${bearerToken}",\n`)
|
||||
}
|
||||
requestString.push(
|
||||
`${requestBody},{ \n headers : {${genHeaders.join("").slice(0, -2)}}\n})`
|
||||
)
|
||||
requestString.push(".then(response => {\n")
|
||||
requestString.push(" console.log(response);\n")
|
||||
requestString.push("})")
|
||||
requestString.push(".catch(e => {\n")
|
||||
requestString.push(" console.error(e);\n")
|
||||
requestString.push("})\n")
|
||||
return requestString.join("")
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import { isJSONContentType } from "~/helpers/utils/contenttypes"
|
||||
|
||||
export const NodejsNativeCodegen = {
|
||||
id: "nodejs-native",
|
||||
name: "NodeJs Native",
|
||||
language: "javascript",
|
||||
generator: ({
|
||||
url,
|
||||
pathName,
|
||||
queryString,
|
||||
auth,
|
||||
httpUser,
|
||||
httpPassword,
|
||||
bearerToken,
|
||||
method,
|
||||
rawInput,
|
||||
rawParams,
|
||||
rawRequestBody,
|
||||
contentType,
|
||||
headers,
|
||||
}) => {
|
||||
const requestString = []
|
||||
const genHeaders = []
|
||||
|
||||
requestString.push(`const http = require('http');\n\n`)
|
||||
|
||||
requestString.push(`const url = '${url}${pathName}${queryString}';\n`)
|
||||
|
||||
requestString.push(`const options = {\n`)
|
||||
requestString.push(` method: '${method}',\n`)
|
||||
|
||||
if (auth === "Basic Auth") {
|
||||
const basic = `${httpUser}:${httpPassword}`
|
||||
genHeaders.push(
|
||||
` "Authorization": "Basic ${window.btoa(
|
||||
unescape(encodeURIComponent(basic))
|
||||
)}",\n`
|
||||
)
|
||||
} else if (auth === "Bearer Token" || auth === "OAuth 2.0") {
|
||||
genHeaders.push(` "Authorization": "Bearer ${bearerToken}",\n`)
|
||||
}
|
||||
|
||||
let requestBody
|
||||
if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
|
||||
requestBody = rawInput ? rawParams : rawRequestBody
|
||||
if (isJSONContentType(contentType)) {
|
||||
requestBody = `JSON.stringify(${requestBody})`
|
||||
} else {
|
||||
requestBody = `\`${requestBody}\``
|
||||
}
|
||||
if (contentType) {
|
||||
genHeaders.push(
|
||||
` "Content-Type": "${contentType}; charset=utf-8",\n`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (headers) {
|
||||
headers.forEach(({ key, value }) => {
|
||||
if (key) genHeaders.push(` "${key}": "${value}",\n`)
|
||||
})
|
||||
}
|
||||
if (genHeaders.length > 0 || headers.length > 0) {
|
||||
requestString.push(
|
||||
` headers: {\n${genHeaders.join("").slice(0, -2)}\n }`
|
||||
)
|
||||
}
|
||||
requestString.push(`};\n\n`)
|
||||
|
||||
requestString.push(
|
||||
`const request = http.request(url, options, (response) => {\n`
|
||||
)
|
||||
requestString.push(` console.log(response);\n`)
|
||||
requestString.push(`});\n\n`)
|
||||
|
||||
requestString.push(`request.on('error', (e) => {\n`)
|
||||
requestString.push(` console.error(e);\n`)
|
||||
requestString.push(`});\n`)
|
||||
|
||||
if (requestBody) {
|
||||
requestString.push(`\nrequest.write(${requestBody});\n`)
|
||||
}
|
||||
|
||||
requestString.push(`request.end();`)
|
||||
return requestString.join("")
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { isJSONContentType } from "~/helpers/utils/contenttypes"
|
||||
|
||||
export const NodejsRequestCodegen = {
|
||||
id: "nodejs-request",
|
||||
name: "NodeJs Request",
|
||||
language: "javascript",
|
||||
generator: ({
|
||||
url,
|
||||
pathName,
|
||||
queryString,
|
||||
auth,
|
||||
httpUser,
|
||||
httpPassword,
|
||||
bearerToken,
|
||||
method,
|
||||
rawInput,
|
||||
rawParams,
|
||||
rawRequestBody,
|
||||
contentType,
|
||||
headers,
|
||||
}) => {
|
||||
const requestString = []
|
||||
const genHeaders = []
|
||||
|
||||
requestString.push(`const request = require('request');\n`)
|
||||
requestString.push(`const options = {\n`)
|
||||
requestString.push(` method: '${method.toLowerCase()}',\n`)
|
||||
requestString.push(` url: '${url}${pathName}${queryString}'`)
|
||||
|
||||
if (auth === "Basic Auth") {
|
||||
const basic = `${httpUser}:${httpPassword}`
|
||||
genHeaders.push(
|
||||
` "Authorization": "Basic ${window.btoa(
|
||||
unescape(encodeURIComponent(basic))
|
||||
)}",\n`
|
||||
)
|
||||
} else if (auth === "Bearer Token" || auth === "OAuth 2.0") {
|
||||
genHeaders.push(` "Authorization": "Bearer ${bearerToken}",\n`)
|
||||
}
|
||||
if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
|
||||
let requestBody = rawInput ? rawParams : rawRequestBody
|
||||
let reqBodyType = "formData"
|
||||
if (isJSONContentType(contentType)) {
|
||||
requestBody = `JSON.stringify(${requestBody})`
|
||||
reqBodyType = "body"
|
||||
} else if (contentType.includes("x-www-form-urlencoded")) {
|
||||
const formData = []
|
||||
if (requestBody.includes("=")) {
|
||||
requestBody.split("&").forEach((rq) => {
|
||||
const [key, val] = rq.split("=")
|
||||
formData.push(`"${key}": "${val}"`)
|
||||
})
|
||||
}
|
||||
if (formData.length) {
|
||||
requestBody = `{${formData.join(", ")}}`
|
||||
}
|
||||
reqBodyType = "form"
|
||||
} else if (contentType.includes("application/xml")) {
|
||||
requestBody = `\`${requestBody}\``
|
||||
reqBodyType = "body"
|
||||
}
|
||||
if (contentType) {
|
||||
genHeaders.push(
|
||||
` "Content-Type": "${contentType}; charset=utf-8",\n`
|
||||
)
|
||||
}
|
||||
requestString.push(`,\n ${reqBodyType}: ${requestBody}`)
|
||||
}
|
||||
|
||||
if (headers.length > 0) {
|
||||
headers.forEach(({ key, value }) => {
|
||||
if (key) genHeaders.push(` "${key}": "${value}",\n`)
|
||||
})
|
||||
}
|
||||
if (genHeaders.length > 0 || headers.length > 0) {
|
||||
requestString.push(
|
||||
`,\n headers: {\n${genHeaders.join("").slice(0, -2)}\n }`
|
||||
)
|
||||
}
|
||||
|
||||
requestString.push(`\n}`)
|
||||
requestString.push(`\nrequest(options, (error, response) => {\n`)
|
||||
requestString.push(` if (error) throw new Error(error);\n`)
|
||||
requestString.push(` console.log(response.body);\n`)
|
||||
requestString.push(`});`)
|
||||
return requestString.join("")
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import { isJSONContentType } from "~/helpers/utils/contenttypes"
|
||||
|
||||
export const NodejsUnirestCodegen = {
|
||||
id: "nodejs-unirest",
|
||||
name: "NodeJs Unirest",
|
||||
language: "javascript",
|
||||
generator: ({
|
||||
url,
|
||||
pathName,
|
||||
queryString,
|
||||
auth,
|
||||
httpUser,
|
||||
httpPassword,
|
||||
bearerToken,
|
||||
method,
|
||||
rawInput,
|
||||
rawParams,
|
||||
rawRequestBody,
|
||||
contentType,
|
||||
headers,
|
||||
}) => {
|
||||
const requestString = []
|
||||
const genHeaders = []
|
||||
|
||||
requestString.push(`const unirest = require('unirest');\n`)
|
||||
requestString.push(`const req = unirest(\n`)
|
||||
requestString.push(
|
||||
`'${method.toLowerCase()}', '${url}${pathName}${queryString}')\n`
|
||||
)
|
||||
|
||||
if (auth === "Basic Auth") {
|
||||
const basic = `${httpUser}:${httpPassword}`
|
||||
genHeaders.push(
|
||||
` "Authorization": "Basic ${window.btoa(
|
||||
unescape(encodeURIComponent(basic))
|
||||
)}",\n`
|
||||
)
|
||||
} else if (auth === "Bearer Token" || auth === "OAuth 2.0") {
|
||||
genHeaders.push(` "Authorization": "Bearer ${bearerToken}",\n`)
|
||||
}
|
||||
|
||||
if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
|
||||
let requestBody = rawInput ? rawParams : rawRequestBody
|
||||
let reqBodyType = "formData"
|
||||
if (isJSONContentType(contentType)) {
|
||||
requestBody = `\`${requestBody}\``
|
||||
reqBodyType = "send"
|
||||
} else if (contentType.includes("x-www-form-urlencoded")) {
|
||||
const formData = []
|
||||
if (requestBody.includes("=")) {
|
||||
requestBody.split("&").forEach((rq) => {
|
||||
const [key, val] = rq.split("=")
|
||||
formData.push(`"${key}": "${val}"`)
|
||||
})
|
||||
}
|
||||
if (formData.length) {
|
||||
requestBody = `{${formData.join(", ")}}`
|
||||
}
|
||||
reqBodyType = "send"
|
||||
} else if (contentType.includes("application/xml")) {
|
||||
requestBody = `\`${requestBody}\``
|
||||
reqBodyType = "send"
|
||||
}
|
||||
if (contentType) {
|
||||
genHeaders.push(
|
||||
` "Content-Type": "${contentType}; charset=utf-8",\n`
|
||||
)
|
||||
}
|
||||
requestString.push(`.\n ${reqBodyType}( ${requestBody})`)
|
||||
}
|
||||
|
||||
if (headers.length > 0) {
|
||||
headers.forEach(({ key, value }) => {
|
||||
if (key) genHeaders.push(` "${key}": "${value}",\n`)
|
||||
})
|
||||
}
|
||||
if (genHeaders.length > 0 || headers.length > 0) {
|
||||
requestString.push(
|
||||
`.\n headers({\n${genHeaders.join("").slice(0, -2)}\n }`
|
||||
)
|
||||
}
|
||||
|
||||
requestString.push(`\n)`)
|
||||
requestString.push(`\n.end(function (res) {\n`)
|
||||
requestString.push(` if (res.error) throw new Error(res.error);\n`)
|
||||
requestString.push(` console.log(res.raw_body);\n });\n`)
|
||||
return requestString.join("")
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { isJSONContentType } from "~/helpers/utils/contenttypes"
|
||||
|
||||
export const PhpCurlCodegen = {
|
||||
id: "php-curl",
|
||||
name: "PHP cURL",
|
||||
language: "php",
|
||||
generator: ({
|
||||
url,
|
||||
pathName,
|
||||
queryString,
|
||||
auth,
|
||||
httpUser,
|
||||
httpPassword,
|
||||
bearerToken,
|
||||
method,
|
||||
rawInput,
|
||||
rawParams,
|
||||
rawRequestBody,
|
||||
contentType,
|
||||
headers,
|
||||
}) => {
|
||||
const requestString = []
|
||||
const genHeaders = []
|
||||
|
||||
requestString.push(`<?php\n`)
|
||||
requestString.push(`$curl = curl_init();\n`)
|
||||
requestString.push(`curl_setopt_array($curl, array(\n`)
|
||||
requestString.push(` CURLOPT_URL => "${url}${pathName}${queryString}",\n`)
|
||||
requestString.push(` CURLOPT_RETURNTRANSFER => true,\n`)
|
||||
requestString.push(` CURLOPT_ENCODING => "",\n`)
|
||||
requestString.push(` CURLOPT_MAXREDIRS => 10,\n`)
|
||||
requestString.push(` CURLOPT_TIMEOUT => 0,\n`)
|
||||
requestString.push(` CURLOPT_FOLLOWLOCATION => true,\n`)
|
||||
requestString.push(` CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,\n`)
|
||||
requestString.push(` CURLOPT_CUSTOMREQUEST => "${method}",\n`)
|
||||
|
||||
if (auth === "Basic Auth") {
|
||||
const basic = `${httpUser}:${httpPassword}`
|
||||
genHeaders.push(
|
||||
` "Authorization: Basic ${window.btoa(
|
||||
unescape(encodeURIComponent(basic))
|
||||
)}",\n`
|
||||
)
|
||||
} else if (auth === "Bearer Token" || auth === "OAuth 2.0") {
|
||||
genHeaders.push(` "Authorization: Bearer ${bearerToken}",\n`)
|
||||
}
|
||||
if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
|
||||
let requestBody = rawInput ? rawParams : rawRequestBody
|
||||
|
||||
if (
|
||||
!isJSONContentType(contentType) &&
|
||||
rawInput &&
|
||||
!contentType.includes("x-www-form-urlencoded")
|
||||
) {
|
||||
const toRemove = /[\n {}]/gim
|
||||
const toReplace = /:/gim
|
||||
const parts = requestBody.replace(toRemove, "").replace(toReplace, "=>")
|
||||
requestBody = `array(${parts})`
|
||||
} else if (isJSONContentType(contentType)) {
|
||||
requestBody = JSON.stringify(requestBody)
|
||||
} else if (contentType.includes("x-www-form-urlencoded")) {
|
||||
if (requestBody.includes("=")) {
|
||||
requestBody = `"${requestBody}"`
|
||||
} else {
|
||||
const requestObject = JSON.parse(requestBody)
|
||||
requestBody = `"${Object.keys(requestObject)
|
||||
.map((key) => `${key}=${requestObject[key].toString()}`)
|
||||
.join("&")}"`
|
||||
}
|
||||
}
|
||||
if (contentType) {
|
||||
genHeaders.push(` "Content-Type: ${contentType}; charset=utf-8",\n`)
|
||||
}
|
||||
requestString.push(` CURLOPT_POSTFIELDS => ${requestBody},\n`)
|
||||
}
|
||||
|
||||
if (headers.length > 0) {
|
||||
headers.forEach(({ key, value }) => {
|
||||
if (key) genHeaders.push(` "${key}: ${value}",\n`)
|
||||
})
|
||||
}
|
||||
if (genHeaders.length > 0 || headers.length > 0) {
|
||||
requestString.push(
|
||||
` CURLOPT_HTTPHEADER => array(\n${genHeaders
|
||||
.join("")
|
||||
.slice(0, -2)}\n )\n`
|
||||
)
|
||||
}
|
||||
|
||||
requestString.push(`));\n`)
|
||||
requestString.push(`$response = curl_exec($curl);\n`)
|
||||
requestString.push(`curl_close($curl);\n`)
|
||||
requestString.push(`echo $response;\n`)
|
||||
|
||||
return requestString.join("")
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
export const PowershellRestmethodCodegen = {
|
||||
id: "powershell-restmethod",
|
||||
name: "PowerShell RestMethod",
|
||||
language: "powershell",
|
||||
generator: ({
|
||||
url,
|
||||
pathName,
|
||||
queryString,
|
||||
auth,
|
||||
httpUser,
|
||||
httpPassword,
|
||||
bearerToken,
|
||||
method,
|
||||
rawInput,
|
||||
rawParams,
|
||||
rawRequestBody,
|
||||
contentType,
|
||||
headers,
|
||||
}) => {
|
||||
const methodsWithBody = ["Put", "Post", "Delete"]
|
||||
const formattedMethod =
|
||||
method[0].toUpperCase() + method.substring(1).toLowerCase()
|
||||
const includeBody = methodsWithBody.includes(formattedMethod)
|
||||
const requestString = []
|
||||
let genHeaders = []
|
||||
let variables = ""
|
||||
|
||||
requestString.push(
|
||||
`Invoke-RestMethod -Method '${formattedMethod}' -Uri '${url}${pathName}${queryString}'`
|
||||
)
|
||||
const requestBody = rawInput ? rawParams : rawRequestBody
|
||||
|
||||
if (requestBody.length !== 0 && includeBody) {
|
||||
variables = variables.concat(`$body = @'\n${requestBody}\n'@\n\n`)
|
||||
}
|
||||
if (headers) {
|
||||
headers.forEach(({ key, value }) => {
|
||||
if (key) genHeaders.push(` '${key}' = '${value}'\n`)
|
||||
})
|
||||
}
|
||||
|
||||
if (contentType) {
|
||||
genHeaders.push(` 'Content-Type' = '${contentType}; charset=utf-8'\n`)
|
||||
requestString.push(` -ContentType '${contentType}; charset=utf-8'`)
|
||||
}
|
||||
|
||||
if (auth === "Basic Auth") {
|
||||
const basic = `${httpUser}:${httpPassword}`
|
||||
genHeaders.push(
|
||||
` 'Authorization' = 'Basic ${window.btoa(
|
||||
unescape(encodeURIComponent(basic))
|
||||
)}'\n`
|
||||
)
|
||||
} else if (auth === "Bearer Token" || auth === "OAuth 2.0") {
|
||||
genHeaders.push(` 'Authorization' = 'Bearer ${bearerToken}'\n`)
|
||||
}
|
||||
genHeaders = genHeaders.join("").slice(0, -1)
|
||||
variables = variables.concat(`$headers = @{\n${genHeaders}\n}\n`)
|
||||
requestString.push(` -Headers $headers`)
|
||||
if (includeBody) {
|
||||
requestString.push(` -Body $body`)
|
||||
}
|
||||
return `${variables}\n${requestString.join("")}`
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
import { isJSONContentType } from "~/helpers/utils/contenttypes"
|
||||
|
||||
const printHeaders = (headers) => {
|
||||
if (headers.length) {
|
||||
return [`headers = {\n`, ` ${headers.join(",\n ")}\n`, `}\n`]
|
||||
} else {
|
||||
return [`headers = {}\n`]
|
||||
}
|
||||
}
|
||||
|
||||
export const PythonHttpClientCodegen = {
|
||||
id: "python-http-client",
|
||||
name: "Python http.client",
|
||||
language: "python",
|
||||
generator: ({
|
||||
url,
|
||||
pathName,
|
||||
queryString,
|
||||
auth,
|
||||
httpUser,
|
||||
httpPassword,
|
||||
bearerToken,
|
||||
method,
|
||||
rawInput,
|
||||
rawParams,
|
||||
rawRequestBody,
|
||||
contentType,
|
||||
headers,
|
||||
}) => {
|
||||
const requestString = []
|
||||
const genHeaders = []
|
||||
|
||||
requestString.push(`import http.client\n`)
|
||||
requestString.push(`import mimetypes\n`)
|
||||
|
||||
const currentUrl = new URL(url)
|
||||
const hostname = currentUrl.hostname
|
||||
const port = currentUrl.port
|
||||
if (!port) {
|
||||
requestString.push(`conn = http.client.HTTPSConnection("${hostname}")\n`)
|
||||
} else {
|
||||
requestString.push(
|
||||
`conn = http.client.HTTPSConnection("${hostname}", ${port})\n`
|
||||
)
|
||||
}
|
||||
// auth headers
|
||||
if (auth === "Basic Auth") {
|
||||
const basic = `${httpUser}:${httpPassword}`
|
||||
genHeaders.push(
|
||||
`'Authorization': 'Basic ${window.btoa(
|
||||
unescape(encodeURIComponent(basic))
|
||||
)}'`
|
||||
)
|
||||
} else if (auth === "Bearer Token" || auth === "OAuth 2.0") {
|
||||
genHeaders.push(`'Authorization': 'Bearer ${bearerToken}'`)
|
||||
}
|
||||
|
||||
// custom headers
|
||||
if (headers.length) {
|
||||
headers.forEach(({ key, value }) => {
|
||||
if (key) genHeaders.push(`'${key}': '${value}'`)
|
||||
})
|
||||
}
|
||||
|
||||
// initial request setup
|
||||
let requestBody = rawInput ? rawParams : rawRequestBody
|
||||
if (method === "GET") {
|
||||
requestString.push(...printHeaders(genHeaders))
|
||||
requestString.push(`payload = ''\n`)
|
||||
}
|
||||
if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
|
||||
genHeaders.push(`'Content-Type': '${contentType}'`)
|
||||
requestString.push(...printHeaders(genHeaders))
|
||||
|
||||
if (isJSONContentType(contentType)) {
|
||||
requestBody = JSON.stringify(requestBody)
|
||||
requestString.push(`payload = ${requestBody}\n`)
|
||||
} else if (contentType.includes("x-www-form-urlencoded")) {
|
||||
const formData = []
|
||||
if (requestBody.includes("=")) {
|
||||
requestBody.split("&").forEach((rq) => {
|
||||
const [key, val] = rq.split("=")
|
||||
formData.push(`('${key}', '${val}')`)
|
||||
})
|
||||
}
|
||||
if (formData.length) {
|
||||
requestString.push(`payload = [${formData.join(",\n ")}]\n`)
|
||||
}
|
||||
} else {
|
||||
requestString.push(`paylod = '''${requestBody}'''\n`)
|
||||
}
|
||||
}
|
||||
requestString.push(
|
||||
`conn.request("${method}", "${pathName}${queryString}", payload, headers)\n`
|
||||
)
|
||||
requestString.push(`res = conn.getresponse()\n`)
|
||||
requestString.push(`data = res.read()\n`)
|
||||
requestString.push(`print(data.decode("utf-8"))`)
|
||||
|
||||
return requestString.join("")
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { isJSONContentType } from "~/helpers/utils/contenttypes"
|
||||
|
||||
const printHeaders = (headers) => {
|
||||
if (headers.length) {
|
||||
return [`headers = {\n`, ` ${headers.join(",\n ")}\n`, `}\n`]
|
||||
}
|
||||
|
||||
return []
|
||||
}
|
||||
|
||||
export const PythonRequestsCodegen = {
|
||||
id: "python-requests",
|
||||
name: "Python Requests",
|
||||
language: "python",
|
||||
generator: ({
|
||||
url,
|
||||
pathName,
|
||||
queryString,
|
||||
auth,
|
||||
httpUser,
|
||||
httpPassword,
|
||||
bearerToken,
|
||||
method,
|
||||
rawInput,
|
||||
rawParams,
|
||||
rawRequestBody,
|
||||
contentType,
|
||||
headers,
|
||||
}) => {
|
||||
const requestString = []
|
||||
const genHeaders = []
|
||||
|
||||
requestString.push(`import requests\n\n`)
|
||||
requestString.push(`url = '${url}${pathName}${queryString}'\n`)
|
||||
|
||||
// auth headers
|
||||
if (auth === "Basic Auth") {
|
||||
const basic = `${httpUser}:${httpPassword}`
|
||||
genHeaders.push(
|
||||
`'Authorization': 'Basic ${window.btoa(
|
||||
unescape(encodeURIComponent(basic))
|
||||
)}'`
|
||||
)
|
||||
} else if (auth === "Bearer Token" || auth === "OAuth 2.0") {
|
||||
genHeaders.push(`'Authorization': 'Bearer ${bearerToken}'`)
|
||||
}
|
||||
|
||||
// custom headers
|
||||
if (headers.length) {
|
||||
headers.forEach(({ key, value }) => {
|
||||
if (key) genHeaders.push(`'${key}': '${value}'`)
|
||||
})
|
||||
}
|
||||
|
||||
// initial request setup
|
||||
let requestBody = rawInput ? rawParams : rawRequestBody
|
||||
if (method === "GET") {
|
||||
requestString.push(...printHeaders(genHeaders))
|
||||
requestString.push(`response = requests.request(\n`)
|
||||
requestString.push(` '${method}',\n`)
|
||||
requestString.push(` '${url}${pathName}${queryString}',\n`)
|
||||
}
|
||||
if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
|
||||
genHeaders.push(`'Content-Type': '${contentType}'`)
|
||||
requestString.push(...printHeaders(genHeaders))
|
||||
|
||||
if (isJSONContentType(contentType)) {
|
||||
requestBody = JSON.stringify(requestBody)
|
||||
requestString.push(`data = ${requestBody}\n`)
|
||||
} else if (contentType.includes("x-www-form-urlencoded")) {
|
||||
const formData = []
|
||||
if (requestBody.includes("=")) {
|
||||
requestBody.split("&").forEach((rq) => {
|
||||
const [key, val] = rq.split("=")
|
||||
formData.push(`('${key}', '${val}')`)
|
||||
})
|
||||
}
|
||||
if (formData.length) {
|
||||
requestString.push(`data = [${formData.join(",\n ")}]\n`)
|
||||
}
|
||||
} else {
|
||||
requestString.push(`data = '''${requestBody}'''\n`)
|
||||
}
|
||||
requestString.push(`response = requests.request(\n`)
|
||||
requestString.push(` '${method}',\n`)
|
||||
requestString.push(` '${url}${pathName}${queryString}',\n`)
|
||||
requestString.push(` data=data,\n`)
|
||||
}
|
||||
|
||||
if (genHeaders.length) {
|
||||
requestString.push(` headers=headers,\n`)
|
||||
}
|
||||
|
||||
requestString.push(`)\n\n`)
|
||||
requestString.push(`print(response)`)
|
||||
|
||||
return requestString.join("")
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
export const RubyNetHttpCodeGen = {
|
||||
id: "ruby-net-http",
|
||||
name: "Ruby Net::HTTP",
|
||||
language: "ruby",
|
||||
generator: ({
|
||||
url,
|
||||
pathName,
|
||||
queryString,
|
||||
auth,
|
||||
httpUser,
|
||||
httpPassword,
|
||||
bearerToken,
|
||||
method,
|
||||
rawInput,
|
||||
rawParams,
|
||||
rawRequestBody,
|
||||
contentType,
|
||||
headers,
|
||||
}) => {
|
||||
const requestString = []
|
||||
|
||||
requestString.push(`require 'net/http'\n`)
|
||||
|
||||
// initial request setup
|
||||
let requestBody = rawInput ? rawParams : rawRequestBody
|
||||
requestBody = requestBody.replace(/'/g, "\\'") // escape single-quotes for single-quoted string compatibility
|
||||
|
||||
const verbs = [
|
||||
{ verb: "GET", rbMethod: "Get" },
|
||||
{ verb: "POST", rbMethod: "Post" },
|
||||
{ verb: "PUT", rbMethod: "Put" },
|
||||
{ verb: "PATCH", rbMethod: "Patch" },
|
||||
{ verb: "DELETE", rbMethod: "Delete" },
|
||||
]
|
||||
|
||||
// create URI and request
|
||||
const verb = verbs.find((v) => v.verb === method)
|
||||
requestString.push(`uri = URI.parse('${url}${pathName}${queryString}')\n`)
|
||||
requestString.push(`request = Net::HTTP::${verb.rbMethod}.new(uri)`)
|
||||
|
||||
// content type
|
||||
if (contentType) {
|
||||
requestString.push(`request['Content-Type'] = '${contentType}'`)
|
||||
}
|
||||
|
||||
// custom headers
|
||||
if (headers) {
|
||||
headers.forEach(({ key, value }) => {
|
||||
if (key) {
|
||||
requestString.push(`request['${key}'] = '${value}'`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// authentication
|
||||
if (auth === "Basic Auth") {
|
||||
requestString.push(`request.basic_auth('${httpUser}', '${httpPassword}')`)
|
||||
} else if (auth === "Bearer Token" || auth === "OAuth 2.0") {
|
||||
requestString.push(`request['Authorization'] = 'Bearer ${bearerToken}'`)
|
||||
}
|
||||
|
||||
// set body
|
||||
if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
|
||||
requestString.push(`request.body = '${requestBody}'\n`)
|
||||
}
|
||||
|
||||
// process
|
||||
requestString.push(`http = Net::HTTP.new(uri.host, uri.port)`)
|
||||
requestString.push(`http.use_ssl = uri.is_a?(URI::HTTPS)`)
|
||||
requestString.push(`response = http.request(request)\n`)
|
||||
|
||||
// analyse result
|
||||
requestString.push(`unless response.is_a?(Net::HTTPSuccess) then`)
|
||||
requestString.push(
|
||||
` raise "An error occurred: #{response.code} #{response.message}"`
|
||||
)
|
||||
requestString.push(`else`)
|
||||
requestString.push(` puts response.body`)
|
||||
requestString.push(`end`)
|
||||
|
||||
return requestString.join("\n")
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
export const SalesforceApexCodegen = {
|
||||
id: "salesforce-apex",
|
||||
name: "Salesforce Apex",
|
||||
language: "apex",
|
||||
generator: ({
|
||||
url,
|
||||
pathName,
|
||||
queryString,
|
||||
auth,
|
||||
httpUser,
|
||||
httpPassword,
|
||||
bearerToken,
|
||||
method,
|
||||
rawInput,
|
||||
rawParams,
|
||||
rawRequestBody,
|
||||
contentType,
|
||||
headers,
|
||||
}) => {
|
||||
const requestString = []
|
||||
|
||||
// initial request setup
|
||||
let requestBody = rawInput ? rawParams : rawRequestBody
|
||||
requestBody = JSON.stringify(requestBody)
|
||||
.replace(/^"|"$/g, "")
|
||||
.replace(/\\"/g, '"')
|
||||
.replace(/'/g, "\\'") // Apex uses single quotes for strings
|
||||
|
||||
// create request
|
||||
requestString.push(`HttpRequest request = new HttpRequest();\n`)
|
||||
requestString.push(`request.setMethod('${method}');\n`)
|
||||
requestString.push(
|
||||
`request.setEndpoint('${url}${pathName}${queryString}');\n\n`
|
||||
)
|
||||
|
||||
// authentification
|
||||
if (auth === "Basic Auth") {
|
||||
const basic = `${httpUser}:${httpPassword}`
|
||||
requestString.push(
|
||||
`request.setHeader('Authorization', 'Basic ${window.btoa(
|
||||
unescape(encodeURIComponent(basic))
|
||||
)}');\n`
|
||||
)
|
||||
} else if (auth === "Bearer Token" || auth === "OAuth 2.0") {
|
||||
requestString.push(
|
||||
`request.setHeader('Authorization', 'Bearer ${bearerToken}');\n`
|
||||
)
|
||||
}
|
||||
|
||||
// content type
|
||||
if (contentType) {
|
||||
requestString.push(
|
||||
`request.setHeader('Content-Type', '${contentType}');\n`
|
||||
)
|
||||
}
|
||||
|
||||
// custom headers
|
||||
if (headers) {
|
||||
headers.forEach(({ key, value }) => {
|
||||
if (key) {
|
||||
requestString.push(`request.setHeader('${key}', '${value}');\n`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
requestString.push(`\n`)
|
||||
|
||||
// set body
|
||||
if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
|
||||
requestString.push(`request.setBody('${requestBody}');\n\n`)
|
||||
}
|
||||
|
||||
// process
|
||||
requestString.push(`try {\n`)
|
||||
requestString.push(` Http client = new Http();\n`)
|
||||
requestString.push(` HttpResponse response = client.send(request);\n`)
|
||||
requestString.push(` System.debug(response.getBody());\n`)
|
||||
requestString.push(`} catch (CalloutException ex) {\n`)
|
||||
requestString.push(
|
||||
` System.debug('An error occurred ' + ex.getMessage());\n`
|
||||
)
|
||||
requestString.push(`}`)
|
||||
|
||||
return requestString.join("")
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
export const ShellHttpieCodegen = {
|
||||
id: "shell-httpie",
|
||||
name: "Shell HTTPie",
|
||||
language: "sh",
|
||||
generator: ({
|
||||
url,
|
||||
pathName,
|
||||
queryString,
|
||||
auth,
|
||||
httpUser,
|
||||
httpPassword,
|
||||
bearerToken,
|
||||
method,
|
||||
rawInput,
|
||||
rawParams,
|
||||
rawRequestBody,
|
||||
contentType,
|
||||
headers,
|
||||
}) => {
|
||||
const methodsWithBody = ["POST", "PUT", "PATCH", "DELETE"]
|
||||
const includeBody = methodsWithBody.includes(method)
|
||||
const requestString = []
|
||||
|
||||
let requestBody = rawInput ? rawParams : rawRequestBody
|
||||
requestBody = requestBody.replace(/'/g, "\\'")
|
||||
if (requestBody.length !== 0 && includeBody) {
|
||||
// Send request body via redirected input
|
||||
requestString.push(`echo -n $'${requestBody}' | `)
|
||||
}
|
||||
|
||||
// Executable itself
|
||||
requestString.push(`http`)
|
||||
|
||||
// basic authentication
|
||||
if (auth === "Basic Auth") {
|
||||
requestString.push(` -a ${httpUser}:${httpPassword}`)
|
||||
}
|
||||
|
||||
// URL
|
||||
let escapedUrl = `${url}${pathName}${queryString}`
|
||||
escapedUrl = escapedUrl.replace(/'/g, "\\'")
|
||||
requestString.push(` ${method} $'${escapedUrl}'`)
|
||||
|
||||
// All headers
|
||||
if (contentType) {
|
||||
requestString.push(` 'Content-Type:${contentType}; charset=utf-8'`)
|
||||
}
|
||||
|
||||
if (headers) {
|
||||
headers.forEach(({ key, value }) => {
|
||||
requestString.push(
|
||||
` $'${key.replace(/'/g, "\\'")}:${value.replace(/'/g, "\\'")}'`
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
if (auth === "Bearer Token" || auth === "OAuth 2.0") {
|
||||
requestString.push(` 'Authorization:Bearer ${bearerToken}'`)
|
||||
}
|
||||
|
||||
return requestString.join("")
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
export const ShellWgetCodegen = {
|
||||
id: "shell-wget",
|
||||
name: "Shell wget",
|
||||
language: "sh",
|
||||
generator: ({
|
||||
url,
|
||||
pathName,
|
||||
queryString,
|
||||
auth,
|
||||
httpUser,
|
||||
httpPassword,
|
||||
bearerToken,
|
||||
method,
|
||||
rawInput,
|
||||
rawParams,
|
||||
rawRequestBody,
|
||||
contentType,
|
||||
headers,
|
||||
}) => {
|
||||
const requestString = []
|
||||
requestString.push(`wget -O - --method=${method}`)
|
||||
requestString.push(` '${url}${pathName}${queryString}'`)
|
||||
if (auth === "Basic Auth") {
|
||||
const basic = `${httpUser}:${httpPassword}`
|
||||
requestString.push(
|
||||
` --header='Authorization: Basic ${window.btoa(
|
||||
unescape(encodeURIComponent(basic))
|
||||
)}'`
|
||||
)
|
||||
} else if (auth === "Bearer Token" || auth === "OAuth 2.0") {
|
||||
requestString.push(` --header='Authorization: Bearer ${bearerToken}'`)
|
||||
}
|
||||
if (headers) {
|
||||
headers.forEach(({ key, value }) => {
|
||||
if (key) requestString.push(` --header='${key}: ${value}'`)
|
||||
})
|
||||
}
|
||||
if (["POST", "PUT", "PATCH", "DELETE"].includes(method)) {
|
||||
const requestBody = rawInput ? rawParams : rawRequestBody
|
||||
requestString.push(
|
||||
` --header='Content-Type: ${contentType}; charset=utf-8'`
|
||||
)
|
||||
requestString.push(` --body-data='${requestBody}'`)
|
||||
}
|
||||
return requestString.join(" \\\n")
|
||||
},
|
||||
}
|
||||
236
packages/hoppscotch-app/helpers/curlparser.ts
Normal file
236
packages/hoppscotch-app/helpers/curlparser.ts
Normal file
@@ -0,0 +1,236 @@
|
||||
import * as URL from "url"
|
||||
import * as querystring from "querystring"
|
||||
import * as cookie from "cookie"
|
||||
import parser from "yargs-parser"
|
||||
|
||||
/**
|
||||
* given this: [ 'msg1=value1', 'msg2=value2' ]
|
||||
* output this: 'msg1=value1&msg2=value2'
|
||||
* @param dataArguments
|
||||
*/
|
||||
const joinDataArguments = (dataArguments: string[]) => {
|
||||
let data = ""
|
||||
dataArguments.forEach((argument, i) => {
|
||||
if (i === 0) {
|
||||
data += argument
|
||||
} else {
|
||||
data += `&${argument}`
|
||||
}
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
const parseDataFromArguments = (parsedArguments: any) => {
|
||||
if (parsedArguments.data) {
|
||||
return {
|
||||
data: Array.isArray(parsedArguments.data)
|
||||
? joinDataArguments(parsedArguments.data)
|
||||
: parsedArguments.data,
|
||||
dataArray: Array.isArray(parsedArguments.data)
|
||||
? parsedArguments.data
|
||||
: null,
|
||||
isDataBinary: false,
|
||||
}
|
||||
} else if (parsedArguments["data-binary"]) {
|
||||
return {
|
||||
data: Array.isArray(parsedArguments["data-binary"])
|
||||
? joinDataArguments(parsedArguments["data-binary"])
|
||||
: parsedArguments["data-binary"],
|
||||
dataArray: Array.isArray(parsedArguments["data-binary"])
|
||||
? parsedArguments["data-binary"]
|
||||
: null,
|
||||
isDataBinary: true,
|
||||
}
|
||||
} else if (parsedArguments.d) {
|
||||
return {
|
||||
data: Array.isArray(parsedArguments.d)
|
||||
? joinDataArguments(parsedArguments.d)
|
||||
: parsedArguments.d,
|
||||
dataArray: Array.isArray(parsedArguments.d) ? parsedArguments.d : null,
|
||||
isDataBinary: false,
|
||||
}
|
||||
} else if (parsedArguments["data-ascii"]) {
|
||||
return {
|
||||
data: Array.isArray(parsedArguments["data-ascii"])
|
||||
? joinDataArguments(parsedArguments["data-ascii"])
|
||||
: parsedArguments["data-ascii"],
|
||||
dataArray: Array.isArray(parsedArguments["data-ascii"])
|
||||
? parsedArguments["data-ascii"]
|
||||
: null,
|
||||
isDataBinary: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const parseCurlCommand = (curlCommand: string) => {
|
||||
const newlineFound = /\\/gi.test(curlCommand)
|
||||
if (newlineFound) {
|
||||
// remove '\' and newlines
|
||||
curlCommand = curlCommand.replace(/\\/gi, "")
|
||||
curlCommand = curlCommand.replace(/\n/g, "")
|
||||
}
|
||||
// yargs parses -XPOST as separate arguments. just prescreen for it.
|
||||
curlCommand = curlCommand.replace(/ -XPOST/, " -X POST")
|
||||
curlCommand = curlCommand.replace(/ -XGET/, " -X GET")
|
||||
curlCommand = curlCommand.replace(/ -XPUT/, " -X PUT")
|
||||
curlCommand = curlCommand.replace(/ -XPATCH/, " -X PATCH")
|
||||
curlCommand = curlCommand.replace(/ -XDELETE/, " -X DELETE")
|
||||
curlCommand = curlCommand.trim()
|
||||
const parsedArguments = parser(curlCommand)
|
||||
let cookieString
|
||||
let cookies
|
||||
let url = parsedArguments._[1]
|
||||
if (!url) {
|
||||
for (const argName in parsedArguments) {
|
||||
if (typeof parsedArguments[argName] === "string") {
|
||||
if (["http", "www."].includes(parsedArguments[argName])) {
|
||||
url = parsedArguments[argName]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let headers: any
|
||||
|
||||
const parseHeaders = (headerFieldName: string) => {
|
||||
if (parsedArguments[headerFieldName]) {
|
||||
if (!headers) {
|
||||
headers = {}
|
||||
}
|
||||
if (!Array.isArray(parsedArguments[headerFieldName])) {
|
||||
parsedArguments[headerFieldName] = [parsedArguments[headerFieldName]]
|
||||
}
|
||||
parsedArguments[headerFieldName].forEach((header: string) => {
|
||||
if (header.includes("Cookie")) {
|
||||
// stupid javascript tricks: closure
|
||||
cookieString = header
|
||||
} else {
|
||||
const colonIndex = header.indexOf(":")
|
||||
const headerName = header.substring(0, colonIndex)
|
||||
const headerValue = header.substring(colonIndex + 1).trim()
|
||||
headers[headerName] = headerValue
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
parseHeaders("H")
|
||||
parseHeaders("header")
|
||||
if (parsedArguments.A) {
|
||||
if (!headers) {
|
||||
headers = []
|
||||
}
|
||||
headers["User-Agent"] = parsedArguments.A
|
||||
} else if (parsedArguments["user-agent"]) {
|
||||
if (!headers) {
|
||||
headers = []
|
||||
}
|
||||
headers["User-Agent"] = parsedArguments["user-agent"]
|
||||
}
|
||||
|
||||
if (parsedArguments.b) {
|
||||
cookieString = parsedArguments.b
|
||||
}
|
||||
if (parsedArguments.cookie) {
|
||||
cookieString = parsedArguments.cookie
|
||||
}
|
||||
const multipartUploads: Record<string, string> = {}
|
||||
if (parsedArguments.F) {
|
||||
if (!Array.isArray(parsedArguments.F)) {
|
||||
parsedArguments.F = [parsedArguments.F]
|
||||
}
|
||||
parsedArguments.F.forEach((multipartArgument: string) => {
|
||||
// input looks like key=value. value could be json or a file path prepended with an @
|
||||
const [key, value] = multipartArgument.split("=", 2)
|
||||
multipartUploads[key] = value
|
||||
})
|
||||
}
|
||||
if (cookieString) {
|
||||
const cookieParseOptions = {
|
||||
decode: (s: any) => s,
|
||||
}
|
||||
// separate out cookie headers into separate data structure
|
||||
// note: cookie is case insensitive
|
||||
cookies = cookie.parse(
|
||||
cookieString.replace(/^Cookie: /gi, ""),
|
||||
cookieParseOptions
|
||||
)
|
||||
}
|
||||
let method
|
||||
if (parsedArguments.X === "POST") {
|
||||
method = "post"
|
||||
} else if (parsedArguments.X === "PUT" || parsedArguments.T) {
|
||||
method = "put"
|
||||
} else if (parsedArguments.X === "PATCH") {
|
||||
method = "patch"
|
||||
} else if (parsedArguments.X === "DELETE") {
|
||||
method = "delete"
|
||||
} else if (parsedArguments.X === "OPTIONS") {
|
||||
method = "options"
|
||||
} else if (
|
||||
(parsedArguments.d ||
|
||||
parsedArguments.data ||
|
||||
parsedArguments["data-ascii"] ||
|
||||
parsedArguments["data-binary"] ||
|
||||
parsedArguments.F ||
|
||||
parsedArguments.form) &&
|
||||
!(parsedArguments.G || parsedArguments.get)
|
||||
) {
|
||||
method = "post"
|
||||
} else if (parsedArguments.I || parsedArguments.head) {
|
||||
method = "head"
|
||||
} else {
|
||||
method = "get"
|
||||
}
|
||||
|
||||
const compressed = !!parsedArguments.compressed
|
||||
let urlObject = URL.parse(url) // eslint-disable-line
|
||||
|
||||
// if GET request with data, convert data to query string
|
||||
// NB: the -G flag does not change the http verb. It just moves the data into the url.
|
||||
if (parsedArguments.G || parsedArguments.get) {
|
||||
urlObject.query = urlObject.query ? urlObject.query : ""
|
||||
const option =
|
||||
"d" in parsedArguments ? "d" : "data" in parsedArguments ? "data" : null
|
||||
if (option) {
|
||||
let urlQueryString = ""
|
||||
|
||||
if (!url.includes("?")) {
|
||||
url += "?"
|
||||
} else {
|
||||
urlQueryString += "&"
|
||||
}
|
||||
|
||||
if (typeof parsedArguments[option] === "object") {
|
||||
urlQueryString += parsedArguments[option].join("&")
|
||||
} else {
|
||||
urlQueryString += parsedArguments[option]
|
||||
}
|
||||
urlObject.query += urlQueryString
|
||||
url += urlQueryString
|
||||
delete parsedArguments[option]
|
||||
}
|
||||
}
|
||||
const query = querystring.parse(urlObject.query!, null as any, null as any, {
|
||||
maxKeys: 10000,
|
||||
})
|
||||
|
||||
urlObject.search = null // Clean out the search/query portion.
|
||||
const request = {
|
||||
url,
|
||||
urlWithoutQuery: URL.format(urlObject),
|
||||
compressed,
|
||||
query,
|
||||
headers,
|
||||
method,
|
||||
cookies,
|
||||
cookieString: cookieString?.replace("Cookie: ", ""),
|
||||
multipartUploads,
|
||||
...parseDataFromArguments(parsedArguments),
|
||||
auth: parsedArguments.u,
|
||||
user: parsedArguments.user,
|
||||
}
|
||||
|
||||
return request
|
||||
}
|
||||
|
||||
export default parseCurlCommand
|
||||
12
packages/hoppscotch-app/helpers/editorutils.js
Normal file
12
packages/hoppscotch-app/helpers/editorutils.js
Normal file
@@ -0,0 +1,12 @@
|
||||
const mimeToMode = {
|
||||
"text/plain": "plain_text",
|
||||
"text/html": "html",
|
||||
"application/xml": "xml",
|
||||
"application/hal+json": "json",
|
||||
"application/vnd.api+json": "json",
|
||||
"application/json": "json",
|
||||
}
|
||||
|
||||
export function getEditorLangForMimeType(mimeType) {
|
||||
return mimeToMode[mimeType] || "plain_text"
|
||||
}
|
||||
107
packages/hoppscotch-app/helpers/fb/analytics.ts
Normal file
107
packages/hoppscotch-app/helpers/fb/analytics.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import {
|
||||
Analytics,
|
||||
getAnalytics,
|
||||
logEvent,
|
||||
setAnalyticsCollectionEnabled,
|
||||
setUserId,
|
||||
setUserProperties,
|
||||
} from "firebase/analytics"
|
||||
import { authEvents$ } from "./auth"
|
||||
import {
|
||||
HoppAccentColor,
|
||||
HoppBgColor,
|
||||
settings$,
|
||||
settingsStore,
|
||||
} from "~/newstore/settings"
|
||||
|
||||
let analytics: Analytics | null = null
|
||||
|
||||
type SettingsCustomDimensions = {
|
||||
usesProxy: boolean
|
||||
usesExtension: boolean
|
||||
syncCollections: boolean
|
||||
syncEnvironments: boolean
|
||||
syncHistory: boolean
|
||||
usesBg: HoppBgColor
|
||||
usesAccent: HoppAccentColor
|
||||
usesTelemetry: boolean
|
||||
}
|
||||
|
||||
type HoppRequestEvent =
|
||||
| {
|
||||
platform: "rest" | "graphql-query" | "graphql-schema"
|
||||
strategy: "normal" | "proxy" | "extension"
|
||||
}
|
||||
| { platform: "wss" | "sse" | "socketio" | "mqtt" }
|
||||
|
||||
export function initAnalytics() {
|
||||
analytics = getAnalytics()
|
||||
|
||||
initLoginListeners()
|
||||
initSettingsListeners()
|
||||
}
|
||||
|
||||
function initLoginListeners() {
|
||||
authEvents$.subscribe((ev) => {
|
||||
if (ev.event === "login") {
|
||||
if (settingsStore.value.TELEMETRY_ENABLED && analytics) {
|
||||
setUserId(analytics, ev.user.uid)
|
||||
|
||||
logEvent(analytics, "login", {
|
||||
method: ev.user.providerData[0]?.providerId, // Assume the first provider is the login provider
|
||||
})
|
||||
}
|
||||
} else if (ev.event === "logout") {
|
||||
if (settingsStore.value.TELEMETRY_ENABLED && analytics) {
|
||||
logEvent(analytics, "logout")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function initSettingsListeners() {
|
||||
// Keep track of the telemetry status
|
||||
let telemetryStatus = settingsStore.value.TELEMETRY_ENABLED
|
||||
|
||||
settings$.subscribe((settings) => {
|
||||
const conf: SettingsCustomDimensions = {
|
||||
usesProxy: settings.PROXY_ENABLED,
|
||||
usesExtension: settings.EXTENSIONS_ENABLED,
|
||||
syncCollections: settings.syncCollections,
|
||||
syncEnvironments: settings.syncEnvironments,
|
||||
syncHistory: settings.syncHistory,
|
||||
usesAccent: settings.THEME_COLOR,
|
||||
usesBg: settings.BG_COLOR,
|
||||
usesTelemetry: settings.TELEMETRY_ENABLED,
|
||||
}
|
||||
|
||||
// User toggled telemetry mode to off or to on
|
||||
if (
|
||||
((telemetryStatus && !settings.TELEMETRY_ENABLED) ||
|
||||
settings.TELEMETRY_ENABLED) &&
|
||||
analytics
|
||||
) {
|
||||
setUserProperties(analytics, conf)
|
||||
}
|
||||
|
||||
telemetryStatus = settings.TELEMETRY_ENABLED
|
||||
|
||||
if (analytics) setAnalyticsCollectionEnabled(analytics, telemetryStatus)
|
||||
})
|
||||
|
||||
if (analytics) setAnalyticsCollectionEnabled(analytics, telemetryStatus)
|
||||
}
|
||||
|
||||
export function logHoppRequestRunToAnalytics(ev: HoppRequestEvent) {
|
||||
if (settingsStore.value.TELEMETRY_ENABLED && analytics) {
|
||||
logEvent(analytics, "hopp-request", ev)
|
||||
}
|
||||
}
|
||||
|
||||
export function logPageView(pagePath: string) {
|
||||
if (settingsStore.value.TELEMETRY_ENABLED && analytics) {
|
||||
logEvent(analytics, "page_view", {
|
||||
page_path: pagePath,
|
||||
})
|
||||
}
|
||||
}
|
||||
285
packages/hoppscotch-app/helpers/fb/auth.ts
Normal file
285
packages/hoppscotch-app/helpers/fb/auth.ts
Normal file
@@ -0,0 +1,285 @@
|
||||
import {
|
||||
User,
|
||||
getAuth,
|
||||
onAuthStateChanged,
|
||||
onIdTokenChanged,
|
||||
signInWithPopup,
|
||||
GoogleAuthProvider,
|
||||
GithubAuthProvider,
|
||||
signInWithEmailAndPassword as signInWithEmailAndPass,
|
||||
isSignInWithEmailLink as isSignInWithEmailLinkFB,
|
||||
fetchSignInMethodsForEmail,
|
||||
sendSignInLinkToEmail,
|
||||
signInWithEmailLink as signInWithEmailLinkFB,
|
||||
ActionCodeSettings,
|
||||
signOut,
|
||||
linkWithCredential,
|
||||
AuthCredential,
|
||||
UserCredential,
|
||||
} from "firebase/auth"
|
||||
import {
|
||||
onSnapshot,
|
||||
getFirestore,
|
||||
setDoc,
|
||||
doc,
|
||||
updateDoc,
|
||||
} from "firebase/firestore"
|
||||
import {
|
||||
BehaviorSubject,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
map,
|
||||
Subject,
|
||||
Subscription,
|
||||
} from "rxjs"
|
||||
import { onBeforeUnmount, onMounted } from "@nuxtjs/composition-api"
|
||||
|
||||
export type HoppUser = User & {
|
||||
provider?: string
|
||||
accessToken?: string
|
||||
}
|
||||
|
||||
type AuthEvents =
|
||||
| { event: "login"; user: HoppUser }
|
||||
| { event: "logout" }
|
||||
| { event: "authTokenUpdate"; user: HoppUser; newToken: string | null }
|
||||
|
||||
/**
|
||||
* A BehaviorSubject emitting the currently logged in user (or null if not logged in)
|
||||
*/
|
||||
export const currentUser$ = new BehaviorSubject<HoppUser | null>(null)
|
||||
/**
|
||||
* A BehaviorSubject emitting the current idToken
|
||||
*/
|
||||
export const authIdToken$ = new BehaviorSubject<string | null>(null)
|
||||
|
||||
/**
|
||||
* A subject that emits events related to authentication flows
|
||||
*/
|
||||
export const authEvents$ = new Subject<AuthEvents>()
|
||||
|
||||
/**
|
||||
* Initializes the firebase authentication related subjects
|
||||
*/
|
||||
export function initAuth() {
|
||||
const auth = getAuth()
|
||||
const firestore = getFirestore()
|
||||
|
||||
let extraSnapshotStop: (() => void) | null = null
|
||||
|
||||
onAuthStateChanged(auth, (user) => {
|
||||
/** Whether the user was logged in before */
|
||||
const wasLoggedIn = currentUser$.value !== null
|
||||
|
||||
if (!user && extraSnapshotStop) {
|
||||
extraSnapshotStop()
|
||||
extraSnapshotStop = null
|
||||
} else if (user) {
|
||||
// Merge all the user info from all the authenticated providers
|
||||
user.providerData.forEach((profile) => {
|
||||
if (!profile) return
|
||||
|
||||
const us = {
|
||||
updatedOn: new Date(),
|
||||
provider: profile.providerId,
|
||||
name: profile.displayName,
|
||||
email: profile.email,
|
||||
photoUrl: profile.photoURL,
|
||||
uid: profile.uid,
|
||||
}
|
||||
|
||||
setDoc(doc(firestore, "users", user.uid), us, { merge: true }).catch(
|
||||
(e) => console.error("error updating", us, e)
|
||||
)
|
||||
})
|
||||
|
||||
extraSnapshotStop = onSnapshot(
|
||||
doc(firestore, "users", user.uid),
|
||||
(doc) => {
|
||||
const data = doc.data()
|
||||
|
||||
const userUpdate: HoppUser = user
|
||||
|
||||
if (data) {
|
||||
// Write extra provider data
|
||||
userUpdate.provider = data.provider
|
||||
userUpdate.accessToken = data.accessToken
|
||||
}
|
||||
|
||||
currentUser$.next(userUpdate)
|
||||
}
|
||||
)
|
||||
}
|
||||
currentUser$.next(user)
|
||||
|
||||
// User wasn't found before, but now is there (login happened)
|
||||
if (!wasLoggedIn && user) {
|
||||
authEvents$.next({
|
||||
event: "login",
|
||||
user: currentUser$.value!!,
|
||||
})
|
||||
} else if (wasLoggedIn && !user) {
|
||||
// User was found before, but now is not there (logout happened)
|
||||
authEvents$.next({
|
||||
event: "logout",
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
onIdTokenChanged(auth, async (user) => {
|
||||
if (user) {
|
||||
authIdToken$.next(await user.getIdToken())
|
||||
|
||||
authEvents$.next({
|
||||
event: "authTokenUpdate",
|
||||
newToken: authIdToken$.value,
|
||||
user: currentUser$.value!!, // Force not-null because user is defined
|
||||
})
|
||||
} else {
|
||||
authIdToken$.next(null)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign user in with a popup using Google
|
||||
*/
|
||||
export async function signInUserWithGoogle() {
|
||||
return await signInWithPopup(getAuth(), new GoogleAuthProvider())
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign user in with a popup using Github
|
||||
*/
|
||||
export async function signInUserWithGithub() {
|
||||
return await signInWithPopup(
|
||||
getAuth(),
|
||||
new GithubAuthProvider().addScope("gist")
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign user in with email and password
|
||||
*/
|
||||
export async function signInWithEmailAndPassword(
|
||||
email: string,
|
||||
password: string
|
||||
) {
|
||||
return await signInWithEmailAndPass(getAuth(), email, password)
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the sign in methods for a given email address
|
||||
*
|
||||
* @param email - Email to get the methods of
|
||||
*
|
||||
* @returns Promise for string array of the auth provider methods accessible
|
||||
*/
|
||||
export async function getSignInMethodsForEmail(email: string) {
|
||||
return await fetchSignInMethodsForEmail(getAuth(), email)
|
||||
}
|
||||
|
||||
export async function linkWithFBCredential(
|
||||
user: User,
|
||||
credential: AuthCredential
|
||||
) {
|
||||
return await linkWithCredential(user, credential)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an email with the signin link to the user
|
||||
*
|
||||
* @param email - Email to send the email to
|
||||
* @param actionCodeSettings - The settings to apply to the link
|
||||
*/
|
||||
export async function signInWithEmail(
|
||||
email: string,
|
||||
actionCodeSettings: ActionCodeSettings
|
||||
) {
|
||||
return await sendSignInLinkToEmail(getAuth(), email, actionCodeSettings)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks and returns whether the sign in link is an email link
|
||||
*
|
||||
* @param url - The URL to look in
|
||||
*/
|
||||
export function isSignInWithEmailLink(url: string) {
|
||||
return isSignInWithEmailLinkFB(getAuth(), url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends an email with sign in with email link
|
||||
*
|
||||
* @param email - Email to log in to
|
||||
* @param url - The action URL which is used to validate login
|
||||
*/
|
||||
export async function signInWithEmailLink(email: string, url: string) {
|
||||
return await signInWithEmailLinkFB(getAuth(), email, url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs out the user
|
||||
*/
|
||||
export async function signOutUser() {
|
||||
if (!currentUser$.value) throw new Error("No user has logged in")
|
||||
|
||||
await signOut(getAuth())
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the provider id and relevant provider auth token
|
||||
* as user metadata
|
||||
*
|
||||
* @param id - The provider ID
|
||||
* @param token - The relevant auth token for the given provider
|
||||
*/
|
||||
export async function setProviderInfo(id: string, token: string) {
|
||||
if (!currentUser$.value) throw new Error("No user has logged in")
|
||||
|
||||
const us = {
|
||||
updatedOn: new Date(),
|
||||
provider: id,
|
||||
accessToken: token,
|
||||
}
|
||||
|
||||
try {
|
||||
await updateDoc(
|
||||
doc(getFirestore(), "users", currentUser$.value.uid),
|
||||
us
|
||||
).catch((e) => console.error("error updating", us, e))
|
||||
} catch (e) {
|
||||
console.error("error updating", e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
export function getGithubCredentialFromResult(result: UserCredential) {
|
||||
return GithubAuthProvider.credentialFromResult(result)
|
||||
}
|
||||
|
||||
/**
|
||||
* A Vue composable function that is called when the auth status
|
||||
* is being updated to being logged in (fired multiple times),
|
||||
* this is also called on component mount if the login
|
||||
* was already resolved before mount.
|
||||
*/
|
||||
export function onLoggedIn(exec: (user: HoppUser) => void) {
|
||||
let sub: Subscription | null = null
|
||||
|
||||
onMounted(() => {
|
||||
sub = currentUser$
|
||||
.pipe(
|
||||
map((user) => !!user), // Get a logged in status (true or false)
|
||||
distinctUntilChanged(), // Don't propagate unless the status updates
|
||||
filter((x) => x) // Don't propagate unless it is logged in
|
||||
)
|
||||
.subscribe(() => {
|
||||
exec(currentUser$.value!)
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
sub?.unsubscribe()
|
||||
})
|
||||
}
|
||||
154
packages/hoppscotch-app/helpers/fb/collections.ts
Normal file
154
packages/hoppscotch-app/helpers/fb/collections.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import {
|
||||
collection,
|
||||
doc,
|
||||
getFirestore,
|
||||
onSnapshot,
|
||||
setDoc,
|
||||
} from "firebase/firestore"
|
||||
import { currentUser$ } from "./auth"
|
||||
import {
|
||||
restCollections$,
|
||||
graphqlCollections$,
|
||||
setRESTCollections,
|
||||
setGraphqlCollections,
|
||||
translateToNewRESTCollection,
|
||||
translateToNewGQLCollection,
|
||||
} from "~/newstore/collections"
|
||||
import { settingsStore } from "~/newstore/settings"
|
||||
|
||||
type CollectionFlags = "collectionsGraphql" | "collections"
|
||||
|
||||
/**
|
||||
* Whether the collections are loaded. If this is set to true
|
||||
* Updates to the collections store are written into firebase.
|
||||
*
|
||||
* If you have want to update the store and not fire the store update
|
||||
* subscription, set this variable to false, do the update and then
|
||||
* set it to true
|
||||
*/
|
||||
let loadedRESTCollections = false
|
||||
|
||||
/**
|
||||
* Whether the collections are loaded. If this is set to true
|
||||
* Updates to the collections store are written into firebase.
|
||||
*
|
||||
* If you have want to update the store and not fire the store update
|
||||
* subscription, set this variable to false, do the update and then
|
||||
* set it to true
|
||||
*/
|
||||
let loadedGraphqlCollections = false
|
||||
|
||||
export async function writeCollections(
|
||||
collection: any[],
|
||||
flag: CollectionFlags
|
||||
) {
|
||||
if (currentUser$.value === null)
|
||||
throw new Error("User not logged in to write collections")
|
||||
|
||||
const cl = {
|
||||
updatedOn: new Date(),
|
||||
author: currentUser$.value.uid,
|
||||
author_name: currentUser$.value.displayName,
|
||||
author_image: currentUser$.value.photoURL,
|
||||
collection,
|
||||
}
|
||||
|
||||
try {
|
||||
await setDoc(
|
||||
doc(getFirestore(), "users", currentUser$.value.uid, flag, "sync"),
|
||||
cl
|
||||
)
|
||||
} catch (e) {
|
||||
console.error("error updating", cl, e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
export function initCollections() {
|
||||
restCollections$.subscribe((collections) => {
|
||||
if (
|
||||
loadedRESTCollections &&
|
||||
currentUser$.value &&
|
||||
settingsStore.value.syncCollections
|
||||
) {
|
||||
writeCollections(collections, "collections")
|
||||
}
|
||||
})
|
||||
|
||||
graphqlCollections$.subscribe((collections) => {
|
||||
if (
|
||||
loadedGraphqlCollections &&
|
||||
currentUser$.value &&
|
||||
settingsStore.value.syncCollections
|
||||
) {
|
||||
writeCollections(collections, "collectionsGraphql")
|
||||
}
|
||||
})
|
||||
|
||||
let restSnapshotStop: (() => void) | null = null
|
||||
let graphqlSnapshotStop: (() => void) | null = null
|
||||
|
||||
currentUser$.subscribe((user) => {
|
||||
if (!user) {
|
||||
if (restSnapshotStop) {
|
||||
restSnapshotStop()
|
||||
restSnapshotStop = null
|
||||
}
|
||||
|
||||
if (graphqlSnapshotStop) {
|
||||
graphqlSnapshotStop()
|
||||
graphqlSnapshotStop = null
|
||||
}
|
||||
} else {
|
||||
restSnapshotStop = onSnapshot(
|
||||
collection(getFirestore(), "users", user.uid, "collections"),
|
||||
(collectionsRef) => {
|
||||
const collections: any[] = []
|
||||
collectionsRef.forEach((doc) => {
|
||||
const collection = doc.data()
|
||||
collection.id = doc.id
|
||||
collections.push(collection)
|
||||
})
|
||||
|
||||
// Prevent infinite ping-pong of updates
|
||||
loadedRESTCollections = false
|
||||
|
||||
// TODO: Wth is with collections[0]
|
||||
if (collections.length > 0) {
|
||||
setRESTCollections(
|
||||
(collections[0].collection ?? []).map(
|
||||
translateToNewRESTCollection
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
loadedRESTCollections = true
|
||||
}
|
||||
)
|
||||
|
||||
graphqlSnapshotStop = onSnapshot(
|
||||
collection(getFirestore(), "users", user.uid, "collectionsGraphql"),
|
||||
(collectionsRef) => {
|
||||
const collections: any[] = []
|
||||
collectionsRef.forEach((doc) => {
|
||||
const collection = doc.data()
|
||||
collection.id = doc.id
|
||||
collections.push(collection)
|
||||
})
|
||||
|
||||
// Prevent infinite ping-pong of updates
|
||||
loadedGraphqlCollections = false
|
||||
|
||||
// TODO: Wth is with collections[0]
|
||||
if (collections.length > 0) {
|
||||
setGraphqlCollections(
|
||||
(collections[0].collection ?? []).map(translateToNewGQLCollection)
|
||||
)
|
||||
}
|
||||
|
||||
loadedGraphqlCollections = true
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
157
packages/hoppscotch-app/helpers/fb/environments.ts
Normal file
157
packages/hoppscotch-app/helpers/fb/environments.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import {
|
||||
collection,
|
||||
doc,
|
||||
getFirestore,
|
||||
onSnapshot,
|
||||
setDoc,
|
||||
} from "firebase/firestore"
|
||||
import { currentUser$ } from "./auth"
|
||||
import {
|
||||
Environment,
|
||||
environments$,
|
||||
globalEnv$,
|
||||
replaceEnvironments,
|
||||
setGlobalEnvVariables,
|
||||
} from "~/newstore/environments"
|
||||
import { settingsStore } from "~/newstore/settings"
|
||||
|
||||
/**
|
||||
* Used locally to prevent infinite loop when environment sync update
|
||||
* is applied to the store which then fires the store sync listener.
|
||||
* When you want to update environments and not want to fire the update listener,
|
||||
* set this to true and then set it back to false once it is done
|
||||
*/
|
||||
let loadedEnvironments = false
|
||||
|
||||
/**
|
||||
* Used locally to prevent infinite loop when global env sync update
|
||||
* is applied to the store which then fires the store sync listener.
|
||||
* When you want to update global env and not want to fire the update listener,
|
||||
* set this to true and then set it back to false once it is done
|
||||
*/
|
||||
let loadedGlobals = true
|
||||
|
||||
async function writeEnvironments(environment: Environment[]) {
|
||||
if (currentUser$.value == null)
|
||||
throw new Error("Cannot write environments when signed out")
|
||||
|
||||
const ev = {
|
||||
updatedOn: new Date(),
|
||||
author: currentUser$.value.uid,
|
||||
author_name: currentUser$.value.displayName,
|
||||
author_image: currentUser$.value.photoURL,
|
||||
environment,
|
||||
}
|
||||
|
||||
try {
|
||||
await setDoc(
|
||||
doc(
|
||||
getFirestore(),
|
||||
"users",
|
||||
currentUser$.value.uid,
|
||||
"envrionments",
|
||||
"sync"
|
||||
),
|
||||
ev
|
||||
)
|
||||
} catch (e) {
|
||||
console.error("error updating", ev, e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
async function writeGlobalEnvironment(variables: Environment["variables"]) {
|
||||
if (currentUser$.value == null)
|
||||
throw new Error("Cannot write global environment when signed out")
|
||||
|
||||
const ev = {
|
||||
updatedOn: new Date(),
|
||||
author: currentUser$.value.uid,
|
||||
author_name: currentUser$.value.displayName,
|
||||
author_image: currentUser$.value.photoURL,
|
||||
variables,
|
||||
}
|
||||
|
||||
try {
|
||||
await setDoc(
|
||||
doc(getFirestore(), "users", currentUser$.value.uid, "globalEnv", "sync"),
|
||||
ev
|
||||
)
|
||||
} catch (e) {
|
||||
console.error("error updating", ev, e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
export function initEnvironments() {
|
||||
environments$.subscribe((envs) => {
|
||||
if (
|
||||
currentUser$.value &&
|
||||
settingsStore.value.syncEnvironments &&
|
||||
loadedEnvironments
|
||||
) {
|
||||
writeEnvironments(envs)
|
||||
}
|
||||
})
|
||||
|
||||
globalEnv$.subscribe((vars) => {
|
||||
if (
|
||||
currentUser$.value &&
|
||||
settingsStore.value.syncEnvironments &&
|
||||
loadedGlobals
|
||||
) {
|
||||
writeGlobalEnvironment(vars)
|
||||
}
|
||||
})
|
||||
|
||||
let envSnapshotStop: (() => void) | null = null
|
||||
let globalsSnapshotStop: (() => void) | null = null
|
||||
|
||||
currentUser$.subscribe((user) => {
|
||||
if (!user) {
|
||||
// User logged out, clean up snapshot listener
|
||||
if (envSnapshotStop) {
|
||||
envSnapshotStop()
|
||||
envSnapshotStop = null
|
||||
}
|
||||
|
||||
if (globalsSnapshotStop) {
|
||||
globalsSnapshotStop()
|
||||
globalsSnapshotStop = null
|
||||
}
|
||||
} else if (user) {
|
||||
envSnapshotStop = onSnapshot(
|
||||
collection(getFirestore(), "users", user.uid, "environments"),
|
||||
(environmentsRef) => {
|
||||
const environments: any[] = []
|
||||
|
||||
environmentsRef.forEach((doc) => {
|
||||
const environment = doc.data()
|
||||
environment.id = doc.id
|
||||
environments.push(environment)
|
||||
})
|
||||
|
||||
loadedEnvironments = false
|
||||
if (environments.length > 0) {
|
||||
replaceEnvironments(environments[0].environment)
|
||||
}
|
||||
loadedEnvironments = true
|
||||
}
|
||||
)
|
||||
globalsSnapshotStop = onSnapshot(
|
||||
collection(getFirestore(), "users", user.uid, "globalEnv"),
|
||||
(globalsRef) => {
|
||||
if (globalsRef.docs.length === 0) {
|
||||
loadedGlobals = true
|
||||
return
|
||||
}
|
||||
|
||||
const doc = globalsRef.docs[0].data()
|
||||
loadedGlobals = false
|
||||
setGlobalEnvVariables(doc.variables)
|
||||
loadedGlobals = true
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
206
packages/hoppscotch-app/helpers/fb/history.ts
Normal file
206
packages/hoppscotch-app/helpers/fb/history.ts
Normal file
@@ -0,0 +1,206 @@
|
||||
import {
|
||||
addDoc,
|
||||
collection,
|
||||
deleteDoc,
|
||||
doc,
|
||||
getDocs,
|
||||
getFirestore,
|
||||
limit,
|
||||
onSnapshot,
|
||||
orderBy,
|
||||
query,
|
||||
updateDoc,
|
||||
} from "firebase/firestore"
|
||||
import { currentUser$ } from "./auth"
|
||||
import { settingsStore } from "~/newstore/settings"
|
||||
import {
|
||||
GQLHistoryEntry,
|
||||
graphqlHistoryStore,
|
||||
HISTORY_LIMIT,
|
||||
RESTHistoryEntry,
|
||||
restHistoryStore,
|
||||
setGraphqlHistoryEntries,
|
||||
setRESTHistoryEntries,
|
||||
translateToNewGQLHistory,
|
||||
translateToNewRESTHistory,
|
||||
} from "~/newstore/history"
|
||||
|
||||
type HistoryFBCollections = "history" | "graphqlHistory"
|
||||
|
||||
/**
|
||||
* Whether the history are loaded. If this is set to true
|
||||
* Updates to the history store are written into firebase.
|
||||
*
|
||||
* If you have want to update the store and not fire the store update
|
||||
* subscription, set this variable to false, do the update and then
|
||||
* set it to true
|
||||
*/
|
||||
let loadedRESTHistory = false
|
||||
|
||||
/**
|
||||
* Whether the history are loaded. If this is set to true
|
||||
* Updates to the history store are written into firebase.
|
||||
*
|
||||
* If you have want to update the store and not fire the store update
|
||||
* subscription, set this variable to false, do the update and then
|
||||
* set it to true
|
||||
*/
|
||||
let loadedGraphqlHistory = false
|
||||
|
||||
async function writeHistory(entry: any, col: HistoryFBCollections) {
|
||||
if (currentUser$.value == null)
|
||||
throw new Error("User not logged in to sync history")
|
||||
|
||||
const hs = {
|
||||
...entry,
|
||||
updatedOn: new Date(),
|
||||
}
|
||||
|
||||
try {
|
||||
await addDoc(
|
||||
collection(getFirestore(), "users", currentUser$.value.uid, col),
|
||||
hs
|
||||
)
|
||||
} catch (e) {
|
||||
console.error("error writing to history", hs, e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteHistory(entry: any, col: HistoryFBCollections) {
|
||||
if (currentUser$.value == null)
|
||||
throw new Error("User not logged in to delete history")
|
||||
|
||||
try {
|
||||
await deleteDoc(
|
||||
doc(getFirestore(), "users", currentUser$.value.uid, col, entry.id)
|
||||
)
|
||||
} catch (e) {
|
||||
console.error("error deleting history", entry, e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
async function clearHistory(col: HistoryFBCollections) {
|
||||
if (currentUser$.value == null)
|
||||
throw new Error("User not logged in to clear history")
|
||||
|
||||
const { docs } = await getDocs(
|
||||
collection(getFirestore(), "users", currentUser$.value.uid, col)
|
||||
)
|
||||
|
||||
await Promise.all(docs.map((e) => deleteHistory(e, col)))
|
||||
}
|
||||
|
||||
async function toggleStar(entry: any, col: HistoryFBCollections) {
|
||||
if (currentUser$.value == null)
|
||||
throw new Error("User not logged in to toggle star")
|
||||
|
||||
try {
|
||||
await updateDoc(
|
||||
doc(getFirestore(), "users", currentUser$.value.uid, col, entry.id),
|
||||
{ star: !entry.star }
|
||||
)
|
||||
} catch (e) {
|
||||
console.error("error toggling star", entry, e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
export function initHistory() {
|
||||
restHistoryStore.dispatches$.subscribe((dispatch) => {
|
||||
if (
|
||||
loadedRESTHistory &&
|
||||
currentUser$.value &&
|
||||
settingsStore.value.syncHistory
|
||||
) {
|
||||
if (dispatch.dispatcher === "addEntry") {
|
||||
writeHistory(dispatch.payload.entry, "history")
|
||||
} else if (dispatch.dispatcher === "deleteEntry") {
|
||||
deleteHistory(dispatch.payload.entry, "history")
|
||||
} else if (dispatch.dispatcher === "clearHistory") {
|
||||
clearHistory("history")
|
||||
} else if (dispatch.dispatcher === "toggleStar") {
|
||||
toggleStar(dispatch.payload.entry, "history")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
graphqlHistoryStore.dispatches$.subscribe((dispatch) => {
|
||||
if (
|
||||
loadedGraphqlHistory &&
|
||||
currentUser$.value &&
|
||||
settingsStore.value.syncHistory
|
||||
) {
|
||||
if (dispatch.dispatcher === "addEntry") {
|
||||
writeHistory(dispatch.payload.entry, "graphqlHistory")
|
||||
} else if (dispatch.dispatcher === "deleteEntry") {
|
||||
deleteHistory(dispatch.payload.entry, "graphqlHistory")
|
||||
} else if (dispatch.dispatcher === "clearHistory") {
|
||||
clearHistory("graphqlHistory")
|
||||
} else if (dispatch.dispatcher === "toggleStar") {
|
||||
toggleStar(dispatch.payload.entry, "graphqlHistory")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
let restSnapshotStop: (() => void) | null = null
|
||||
let graphqlSnapshotStop: (() => void) | null = null
|
||||
|
||||
currentUser$.subscribe((user) => {
|
||||
if (!user) {
|
||||
// Clear the snapshot listeners when the user logs out
|
||||
if (restSnapshotStop) {
|
||||
restSnapshotStop()
|
||||
restSnapshotStop = null
|
||||
}
|
||||
|
||||
if (graphqlSnapshotStop) {
|
||||
graphqlSnapshotStop()
|
||||
graphqlSnapshotStop = null
|
||||
}
|
||||
} else {
|
||||
restSnapshotStop = onSnapshot(
|
||||
query(
|
||||
collection(getFirestore(), "users", user.uid, "history"),
|
||||
orderBy("updatedOn", "desc"),
|
||||
limit(HISTORY_LIMIT)
|
||||
),
|
||||
(historyRef) => {
|
||||
const history: RESTHistoryEntry[] = []
|
||||
|
||||
historyRef.forEach((doc) => {
|
||||
const entry = doc.data()
|
||||
entry.id = doc.id
|
||||
history.push(translateToNewRESTHistory(entry))
|
||||
})
|
||||
|
||||
loadedRESTHistory = false
|
||||
setRESTHistoryEntries(history)
|
||||
loadedRESTHistory = true
|
||||
}
|
||||
)
|
||||
|
||||
graphqlSnapshotStop = onSnapshot(
|
||||
query(
|
||||
collection(getFirestore(), "users", user.uid, "graphqlHistory"),
|
||||
orderBy("updatedOn", "desc"),
|
||||
limit(HISTORY_LIMIT)
|
||||
),
|
||||
(historyRef) => {
|
||||
const history: GQLHistoryEntry[] = []
|
||||
|
||||
historyRef.forEach((doc) => {
|
||||
const entry = doc.data()
|
||||
entry.id = doc.id
|
||||
history.push(translateToNewGQLHistory(entry))
|
||||
})
|
||||
|
||||
loadedGraphqlHistory = false
|
||||
setGraphqlHistoryEntries(history)
|
||||
loadedGraphqlHistory = true
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
40
packages/hoppscotch-app/helpers/fb/index.ts
Normal file
40
packages/hoppscotch-app/helpers/fb/index.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { initializeApp } from "firebase/app"
|
||||
import { initAnalytics } from "./analytics"
|
||||
import { initAuth } from "./auth"
|
||||
import { initCollections } from "./collections"
|
||||
import { initEnvironments } from "./environments"
|
||||
import { initHistory } from "./history"
|
||||
import { initSettings } from "./settings"
|
||||
|
||||
const firebaseConfig = {
|
||||
apiKey: process.env.API_KEY,
|
||||
authDomain: process.env.AUTH_DOMAIN,
|
||||
databaseURL: process.env.DATABASE_URL,
|
||||
projectId: process.env.PROJECT_ID,
|
||||
storageBucket: process.env.STORAGE_BUCKET,
|
||||
messagingSenderId: process.env.MESSAGING_SENDER_ID,
|
||||
appId: process.env.APP_ID,
|
||||
measurementId: process.env.MEASUREMENT_ID,
|
||||
}
|
||||
|
||||
let initialized = false
|
||||
|
||||
export function initializeFirebase() {
|
||||
if (!initialized) {
|
||||
try {
|
||||
initializeApp(firebaseConfig)
|
||||
|
||||
initAuth()
|
||||
initSettings()
|
||||
initCollections()
|
||||
initHistory()
|
||||
initEnvironments()
|
||||
initAnalytics()
|
||||
|
||||
initialized = true
|
||||
} catch (e) {
|
||||
// initializeApp throws exception if we reinitialize
|
||||
initialized = true
|
||||
}
|
||||
}
|
||||
}
|
||||
74
packages/hoppscotch-app/helpers/fb/request.ts
Normal file
74
packages/hoppscotch-app/helpers/fb/request.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import {
|
||||
audit,
|
||||
combineLatest,
|
||||
distinctUntilChanged,
|
||||
EMPTY,
|
||||
from,
|
||||
map,
|
||||
Subscription,
|
||||
} from "rxjs"
|
||||
import { doc, getDoc, getFirestore, setDoc } from "firebase/firestore"
|
||||
import {
|
||||
HoppRESTRequest,
|
||||
translateToNewRequest,
|
||||
} from "../types/HoppRESTRequest"
|
||||
import { currentUser$, HoppUser } from "./auth"
|
||||
import { restRequest$ } from "~/newstore/RESTSession"
|
||||
|
||||
/**
|
||||
* Writes a request to a user's firestore sync
|
||||
*
|
||||
* @param user The user to write to
|
||||
* @param request The request to write to the request sync
|
||||
*/
|
||||
function writeCurrentRequest(user: HoppUser, request: HoppRESTRequest) {
|
||||
return setDoc(
|
||||
doc(getFirestore(), "users", user.uid, "requests", "rest"),
|
||||
request
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the synced request from the firestore sync
|
||||
*
|
||||
* @returns Fetched request object if exists else null
|
||||
*/
|
||||
export async function loadRequestFromSync(): Promise<HoppRESTRequest | null> {
|
||||
const currentUser = currentUser$.value
|
||||
|
||||
if (!currentUser)
|
||||
throw new Error("Cannot load request from sync without login")
|
||||
|
||||
const fbDoc = await getDoc(
|
||||
doc(getFirestore(), "users", currentUser.uid, "requests", "rest")
|
||||
)
|
||||
|
||||
const data = fbDoc.data()
|
||||
|
||||
if (!data) return null
|
||||
else return translateToNewRequest(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs sync of the REST Request session with Firestore.
|
||||
*
|
||||
* @returns A subscription to the sync observable stream.
|
||||
* Unsubscribe to stop syncing.
|
||||
*/
|
||||
export function startRequestSync(): Subscription {
|
||||
const sub = combineLatest([
|
||||
currentUser$,
|
||||
restRequest$.pipe(distinctUntilChanged()),
|
||||
])
|
||||
.pipe(
|
||||
map(([user, request]) =>
|
||||
user ? from(writeCurrentRequest(user, request)) : EMPTY
|
||||
),
|
||||
audit((x) => x)
|
||||
)
|
||||
.subscribe(() => {
|
||||
// NOTE: This subscription should be kept
|
||||
})
|
||||
|
||||
return sub
|
||||
}
|
||||
93
packages/hoppscotch-app/helpers/fb/settings.ts
Normal file
93
packages/hoppscotch-app/helpers/fb/settings.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import {
|
||||
collection,
|
||||
doc,
|
||||
getFirestore,
|
||||
onSnapshot,
|
||||
setDoc,
|
||||
} from "@firebase/firestore"
|
||||
import { currentUser$ } from "./auth"
|
||||
import { applySetting, settingsStore, SettingsType } from "~/newstore/settings"
|
||||
|
||||
/**
|
||||
* Used locally to prevent infinite loop when settings sync update
|
||||
* is applied to the store which then fires the store sync listener.
|
||||
* When you want to update settings and not want to fire the update listener,
|
||||
* set this to true and then set it back to false once it is done
|
||||
*/
|
||||
let loadedSettings = false
|
||||
|
||||
/**
|
||||
* Write Transform
|
||||
*/
|
||||
async function writeSettings(setting: string, value: any) {
|
||||
if (currentUser$.value === null)
|
||||
throw new Error("Cannot write setting, user not signed in")
|
||||
|
||||
const st = {
|
||||
updatedOn: new Date(),
|
||||
author: currentUser$.value.uid,
|
||||
author_name: currentUser$.value.displayName,
|
||||
author_image: currentUser$.value.photoURL,
|
||||
name: setting,
|
||||
value,
|
||||
}
|
||||
|
||||
try {
|
||||
await setDoc(
|
||||
doc(getFirestore(), "users", currentUser$.value.uid, "settings", setting),
|
||||
st
|
||||
)
|
||||
} catch (e) {
|
||||
console.error("error updating", st, e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
export function initSettings() {
|
||||
settingsStore.dispatches$.subscribe((dispatch) => {
|
||||
if (currentUser$.value && loadedSettings) {
|
||||
if (dispatch.dispatcher === "bulkApplySettings") {
|
||||
Object.keys(dispatch.payload).forEach((key) => {
|
||||
writeSettings(key, dispatch.payload[key])
|
||||
})
|
||||
} else {
|
||||
writeSettings(
|
||||
dispatch.payload.settingKey,
|
||||
settingsStore.value[dispatch.payload.settingKey as keyof SettingsType]
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
let snapshotStop: (() => void) | null = null
|
||||
|
||||
// Subscribe and unsubscribe event listeners
|
||||
currentUser$.subscribe((user) => {
|
||||
if (!user && snapshotStop) {
|
||||
// User logged out
|
||||
snapshotStop()
|
||||
snapshotStop = null
|
||||
} else if (user) {
|
||||
snapshotStop = onSnapshot(
|
||||
collection(getFirestore(), "users", user.uid, "settings"),
|
||||
(settingsRef) => {
|
||||
const settings: any[] = []
|
||||
|
||||
settingsRef.forEach((doc) => {
|
||||
const setting = doc.data()
|
||||
setting.id = doc.id
|
||||
settings.push(setting)
|
||||
})
|
||||
|
||||
loadedSettings = false
|
||||
settings.forEach((e) => {
|
||||
if (e && e.name && e.value != null) {
|
||||
applySetting(e.name, e.value)
|
||||
}
|
||||
})
|
||||
loadedSettings = true
|
||||
}
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
37
packages/hoppscotch-app/helpers/findStatusGroup.js
Normal file
37
packages/hoppscotch-app/helpers/findStatusGroup.js
Normal file
@@ -0,0 +1,37 @@
|
||||
export default function (responseStatus) {
|
||||
if (responseStatus >= 100 && responseStatus < 200)
|
||||
return {
|
||||
name: "informational",
|
||||
className: "info-response",
|
||||
}
|
||||
|
||||
if (responseStatus >= 200 && responseStatus < 300)
|
||||
return {
|
||||
name: "successful",
|
||||
className: "success-response",
|
||||
}
|
||||
|
||||
if (responseStatus >= 300 && responseStatus < 400)
|
||||
return {
|
||||
name: "redirection",
|
||||
className: "redir-response",
|
||||
}
|
||||
|
||||
if (responseStatus >= 400 && responseStatus < 500)
|
||||
return {
|
||||
name: "client error",
|
||||
className: "cl-error-response",
|
||||
}
|
||||
|
||||
if (responseStatus >= 500 && responseStatus < 600)
|
||||
return {
|
||||
name: "server error",
|
||||
className: "sv-error-response",
|
||||
}
|
||||
|
||||
// this object is a catch-all for when no other objects match and should always be last
|
||||
return {
|
||||
name: "unknown",
|
||||
className: "missing-data-response",
|
||||
}
|
||||
}
|
||||
124
packages/hoppscotch-app/helpers/headers.js
Normal file
124
packages/hoppscotch-app/helpers/headers.js
Normal file
@@ -0,0 +1,124 @@
|
||||
export const commonHeaders = [
|
||||
"WWW-Authenticate",
|
||||
"Authorization",
|
||||
"Proxy-Authenticate",
|
||||
"Proxy-Authorization",
|
||||
"Age",
|
||||
"Cache-Control",
|
||||
"Clear-Site-Data",
|
||||
"Expires",
|
||||
"Pragma",
|
||||
"Warning",
|
||||
"Accept-CH",
|
||||
"Accept-CH-Lifetime",
|
||||
"Early-Data",
|
||||
"Content-DPR",
|
||||
"DPR",
|
||||
"Device-Memory",
|
||||
"Save-Data",
|
||||
"Viewport-Width",
|
||||
"Width",
|
||||
"Last-Modified",
|
||||
"ETag",
|
||||
"If-Match",
|
||||
"If-None-Match",
|
||||
"If-Modified-Since",
|
||||
"If-Unmodified-Since",
|
||||
"Vary",
|
||||
"Connection",
|
||||
"Keep-Alive",
|
||||
"Accept",
|
||||
"Accept-Charset",
|
||||
"Accept-Encoding",
|
||||
"Accept-Language",
|
||||
"Expect",
|
||||
"Max-Forwards",
|
||||
"Cookie",
|
||||
"Set-Cookie",
|
||||
"Cookie2",
|
||||
"Set-Cookie2",
|
||||
"Access-Control-Allow-Origin",
|
||||
"Access-Control-Allow-Credentials",
|
||||
"Access-Control-Allow-Headers",
|
||||
"Access-Control-Allow-Methods",
|
||||
"Access-Control-Expose-Headers",
|
||||
"Access-Control-Max-Age",
|
||||
"Access-Control-Request-Headers",
|
||||
"Access-Control-Request-Method",
|
||||
"Origin",
|
||||
"Service-Worker-Allowed",
|
||||
"Timing-Allow-Origin",
|
||||
"X-Permitted-Cross-Domain-Policies",
|
||||
"DNT",
|
||||
"Tk",
|
||||
"Content-Disposition",
|
||||
"Content-Length",
|
||||
"Content-Type",
|
||||
"Content-Encoding",
|
||||
"Content-Language",
|
||||
"Content-Location",
|
||||
"Forwarded",
|
||||
"X-Forwarded-For",
|
||||
"X-Forwarded-Host",
|
||||
"X-Forwarded-Proto",
|
||||
"Via",
|
||||
"Location",
|
||||
"From",
|
||||
"Host",
|
||||
"Referer",
|
||||
"Referrer-Policy",
|
||||
"User-Agent",
|
||||
"Allow",
|
||||
"Server",
|
||||
"Accept-Ranges",
|
||||
"Range",
|
||||
"If-Range",
|
||||
"Content-Range",
|
||||
"Cross-Origin-Opener-Policy",
|
||||
"Cross-Origin-Resource-Policy",
|
||||
"Content-Security-Policy",
|
||||
"Content-Security-Policy-Report-Only",
|
||||
"Expect-CT",
|
||||
"Feature-Policy",
|
||||
"Public-Key-Pins",
|
||||
"Public-Key-Pins-Report-Only",
|
||||
"Strict-Transport-Security",
|
||||
"Upgrade-Insecure-Requests",
|
||||
"X-Content-Type-Options",
|
||||
"X-Download-Options",
|
||||
"X-Frame-Options",
|
||||
"X-Powered-By",
|
||||
"X-XSS-Protection",
|
||||
"Last-Event-ID",
|
||||
"NEL",
|
||||
"Ping-From",
|
||||
"Ping-To",
|
||||
"Report-To",
|
||||
"Transfer-Encoding",
|
||||
"TE",
|
||||
"Trailer",
|
||||
"Sec-WebSocket-Key",
|
||||
"Sec-WebSocket-Extensions",
|
||||
"Sec-WebSocket-Accept",
|
||||
"Sec-WebSocket-Protocol",
|
||||
"Sec-WebSocket-Version",
|
||||
"Accept-Push-Policy",
|
||||
"Accept-Signature",
|
||||
"Alt-Svc",
|
||||
"Date",
|
||||
"Large-Allocation",
|
||||
"Link",
|
||||
"Push-Policy",
|
||||
"Retry-After",
|
||||
"Signature",
|
||||
"Signed-Headers",
|
||||
"Server-Timing",
|
||||
"SourceMap",
|
||||
"Upgrade",
|
||||
"X-DNS-Prefetch-Control",
|
||||
"X-Firefox-Spdy",
|
||||
"X-Pingback",
|
||||
"X-Requested-With",
|
||||
"X-Robots-Tag",
|
||||
"X-UA-Compatible",
|
||||
]
|
||||
318
packages/hoppscotch-app/helpers/jsonParse.js
Normal file
318
packages/hoppscotch-app/helpers/jsonParse.js
Normal file
@@ -0,0 +1,318 @@
|
||||
/**
|
||||
* Copyright (c) 2019 GraphQL Contributors
|
||||
* All rights reserved.
|
||||
*
|
||||
* This source code is licensed under the BSD-style license found in the
|
||||
* LICENSE file in the root directory of this source tree. An additional grant
|
||||
* of patent rights can be found in the PATENTS file in the same directory.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This JSON parser simply walks the input, generating an AST. Use this in lieu
|
||||
* of JSON.parse if you need character offset parse errors and an AST parse tree
|
||||
* with location information.
|
||||
*
|
||||
* If an error is encountered, a SyntaxError will be thrown, with properties:
|
||||
*
|
||||
* - message: string
|
||||
* - start: int - the start inclusive offset of the syntax error
|
||||
* - end: int - the end exclusive offset of the syntax error
|
||||
*
|
||||
*/
|
||||
export default function jsonParse(str) {
|
||||
string = str
|
||||
strLen = str.length
|
||||
start = end = lastEnd = -1
|
||||
ch()
|
||||
lex()
|
||||
try {
|
||||
const ast = parseObj()
|
||||
expect("EOF")
|
||||
return ast
|
||||
} catch (e) {
|
||||
// Try parsing expecting a root array
|
||||
const ast = parseArr()
|
||||
expect("EOF")
|
||||
return ast
|
||||
}
|
||||
}
|
||||
|
||||
let string
|
||||
let strLen
|
||||
let start
|
||||
let end
|
||||
let lastEnd
|
||||
let code
|
||||
let kind
|
||||
|
||||
function parseObj() {
|
||||
const nodeStart = start
|
||||
const members = []
|
||||
expect("{")
|
||||
if (!skip("}")) {
|
||||
do {
|
||||
members.push(parseMember())
|
||||
} while (skip(","))
|
||||
expect("}")
|
||||
}
|
||||
return {
|
||||
kind: "Object",
|
||||
start: nodeStart,
|
||||
end: lastEnd,
|
||||
members,
|
||||
}
|
||||
}
|
||||
|
||||
function parseMember() {
|
||||
const nodeStart = start
|
||||
const key = kind === "String" ? curToken() : null
|
||||
expect("String")
|
||||
expect(":")
|
||||
const value = parseVal()
|
||||
return {
|
||||
kind: "Member",
|
||||
start: nodeStart,
|
||||
end: lastEnd,
|
||||
key,
|
||||
value,
|
||||
}
|
||||
}
|
||||
|
||||
function parseArr() {
|
||||
const nodeStart = start
|
||||
const values = []
|
||||
expect("[")
|
||||
if (!skip("]")) {
|
||||
do {
|
||||
values.push(parseVal())
|
||||
} while (skip(","))
|
||||
expect("]")
|
||||
}
|
||||
return {
|
||||
kind: "Array",
|
||||
start: nodeStart,
|
||||
end: lastEnd,
|
||||
values,
|
||||
}
|
||||
}
|
||||
|
||||
function parseVal() {
|
||||
switch (kind) {
|
||||
case "[":
|
||||
return parseArr()
|
||||
case "{":
|
||||
return parseObj()
|
||||
case "String":
|
||||
case "Number":
|
||||
case "Boolean":
|
||||
case "Null":
|
||||
// eslint-disable-next-line no-case-declarations
|
||||
const token = curToken()
|
||||
lex()
|
||||
return token
|
||||
}
|
||||
return expect("Value")
|
||||
}
|
||||
|
||||
function curToken() {
|
||||
return { kind, start, end, value: JSON.parse(string.slice(start, end)) }
|
||||
}
|
||||
|
||||
function expect(str) {
|
||||
if (kind === str) {
|
||||
lex()
|
||||
return
|
||||
}
|
||||
|
||||
let found
|
||||
if (kind === "EOF") {
|
||||
found = "[end of file]"
|
||||
} else if (end - start > 1) {
|
||||
found = `\`${string.slice(start, end)}\``
|
||||
} else {
|
||||
const match = string.slice(start).match(/^.+?\b/)
|
||||
found = `\`${match ? match[0] : string[start]}\``
|
||||
}
|
||||
|
||||
throw syntaxError(`Expected ${str} but found ${found}.`)
|
||||
}
|
||||
|
||||
function syntaxError(message) {
|
||||
return { message, start, end }
|
||||
}
|
||||
|
||||
function skip(k) {
|
||||
if (kind === k) {
|
||||
lex()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
function ch() {
|
||||
if (end < strLen) {
|
||||
end++
|
||||
code = end === strLen ? 0 : string.charCodeAt(end)
|
||||
}
|
||||
}
|
||||
|
||||
function lex() {
|
||||
lastEnd = end
|
||||
|
||||
while (code === 9 || code === 10 || code === 13 || code === 32) {
|
||||
ch()
|
||||
}
|
||||
|
||||
if (code === 0) {
|
||||
kind = "EOF"
|
||||
return
|
||||
}
|
||||
|
||||
start = end
|
||||
|
||||
switch (code) {
|
||||
// "
|
||||
case 34:
|
||||
kind = "String"
|
||||
return readString()
|
||||
// -, 0-9
|
||||
case 45:
|
||||
case 48:
|
||||
case 49:
|
||||
case 50:
|
||||
case 51:
|
||||
case 52:
|
||||
case 53:
|
||||
case 54:
|
||||
case 55:
|
||||
case 56:
|
||||
case 57:
|
||||
kind = "Number"
|
||||
return readNumber()
|
||||
// f
|
||||
case 102:
|
||||
if (string.slice(start, start + 5) !== "false") {
|
||||
break
|
||||
}
|
||||
end += 4
|
||||
ch()
|
||||
|
||||
kind = "Boolean"
|
||||
return
|
||||
// n
|
||||
case 110:
|
||||
if (string.slice(start, start + 4) !== "null") {
|
||||
break
|
||||
}
|
||||
end += 3
|
||||
ch()
|
||||
|
||||
kind = "Null"
|
||||
return
|
||||
// t
|
||||
case 116:
|
||||
if (string.slice(start, start + 4) !== "true") {
|
||||
break
|
||||
}
|
||||
end += 3
|
||||
ch()
|
||||
|
||||
kind = "Boolean"
|
||||
return
|
||||
}
|
||||
|
||||
kind = string[start]
|
||||
ch()
|
||||
}
|
||||
|
||||
function readString() {
|
||||
ch()
|
||||
while (code !== 34 && code > 31) {
|
||||
if (code === 92) {
|
||||
// \
|
||||
ch()
|
||||
switch (code) {
|
||||
case 34: // "
|
||||
case 47: // /
|
||||
case 92: // \
|
||||
case 98: // b
|
||||
case 102: // f
|
||||
case 110: // n
|
||||
case 114: // r
|
||||
case 116: // t
|
||||
ch()
|
||||
break
|
||||
case 117: // u
|
||||
ch()
|
||||
readHex()
|
||||
readHex()
|
||||
readHex()
|
||||
readHex()
|
||||
break
|
||||
default:
|
||||
throw syntaxError("Bad character escape sequence.")
|
||||
}
|
||||
} else if (end === strLen) {
|
||||
throw syntaxError("Unterminated string.")
|
||||
} else {
|
||||
ch()
|
||||
}
|
||||
}
|
||||
|
||||
if (code === 34) {
|
||||
ch()
|
||||
return
|
||||
}
|
||||
|
||||
throw syntaxError("Unterminated string.")
|
||||
}
|
||||
|
||||
function readHex() {
|
||||
if (
|
||||
(code >= 48 && code <= 57) || // 0-9
|
||||
(code >= 65 && code <= 70) || // A-F
|
||||
(code >= 97 && code <= 102) // a-f
|
||||
) {
|
||||
return ch()
|
||||
}
|
||||
throw syntaxError("Expected hexadecimal digit.")
|
||||
}
|
||||
|
||||
function readNumber() {
|
||||
if (code === 45) {
|
||||
// -
|
||||
ch()
|
||||
}
|
||||
|
||||
if (code === 48) {
|
||||
// 0
|
||||
ch()
|
||||
} else {
|
||||
readDigits()
|
||||
}
|
||||
|
||||
if (code === 46) {
|
||||
// .
|
||||
ch()
|
||||
readDigits()
|
||||
}
|
||||
|
||||
if (code === 69 || code === 101) {
|
||||
// E e
|
||||
ch()
|
||||
if (code === 43 || code === 45) {
|
||||
// + -
|
||||
ch()
|
||||
}
|
||||
readDigits()
|
||||
}
|
||||
}
|
||||
|
||||
function readDigits() {
|
||||
if (code < 48 || code > 57) {
|
||||
// 0 - 9
|
||||
throw syntaxError("Expected decimal digit.")
|
||||
}
|
||||
do {
|
||||
ch()
|
||||
} while (code >= 48 && code <= 57) // 0 - 9
|
||||
}
|
||||
171
packages/hoppscotch-app/helpers/keybindings.ts
Normal file
171
packages/hoppscotch-app/helpers/keybindings.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { onBeforeUnmount, onMounted } from "@nuxtjs/composition-api"
|
||||
import { HoppAction, invokeAction } from "./actions"
|
||||
import { isAppleDevice } from "./platformutils"
|
||||
import { isDOMElement, isTypableElement } from "./utils/dom"
|
||||
|
||||
/**
|
||||
* This variable keeps track whether keybindings are being accepted
|
||||
* true -> Keybindings are checked
|
||||
* false -> Key presses are ignored (Keybindings are not checked)
|
||||
*/
|
||||
let keybindingsEnabled = true
|
||||
|
||||
/**
|
||||
* Alt is also regarded as macOS OPTION (⌥) key
|
||||
* Ctrl is also regarded as macOS COMMAND (⌘) key (NOTE: this differs from HTML Keyboard spec where COMMAND is Meta key!)
|
||||
*/
|
||||
type ModifierKeys = "ctrl" | "alt" | "ctrl-shift" | "alt-shift"
|
||||
|
||||
/* eslint-disable prettier/prettier */
|
||||
// prettier-ignore
|
||||
type Key =
|
||||
| "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h" | "i" | "j"
|
||||
| "k" | "l" | "m" | "n" | "o" | "p" | "q" | "r" | "s" | "t"
|
||||
| "u" | "v" | "w" | "x" | "y" | "z" | "0" | "1" | "2" | "3"
|
||||
| "4" | "5" | "6" | "7" | "8" | "9" | "up" | "down" | "left"
|
||||
| "right" | "/" | "?"
|
||||
/* eslint-enable */
|
||||
|
||||
type ModifierBasedShortcutKey = `${ModifierKeys}-${Key}`
|
||||
// Singular keybindings (these will be disabled when an input-ish area has been focused)
|
||||
type SingleCharacterShortcutKey = `${Key}`
|
||||
|
||||
type ShortcutKey = ModifierBasedShortcutKey | SingleCharacterShortcutKey
|
||||
|
||||
export const bindings: {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
[_ in ShortcutKey]?: HoppAction
|
||||
} = {
|
||||
"ctrl-g": "request.send-cancel",
|
||||
"ctrl-i": "request.reset",
|
||||
"ctrl-u": "request.copy-link",
|
||||
"ctrl-s": "request.save",
|
||||
"ctrl-shift-s": "request.save-as",
|
||||
"alt-up": "request.method.next",
|
||||
"alt-down": "request.method.prev",
|
||||
"alt-g": "request.method.get",
|
||||
"alt-h": "request.method.head",
|
||||
"alt-p": "request.method.post",
|
||||
"alt-u": "request.method.put",
|
||||
"alt-x": "request.method.delete",
|
||||
"ctrl-k": "flyouts.keybinds.toggle",
|
||||
"/": "modals.search.toggle",
|
||||
"?": "modals.support.toggle",
|
||||
"ctrl-m": "modals.share.toggle",
|
||||
"alt-r": "navigation.jump.rest",
|
||||
"alt-q": "navigation.jump.graphql",
|
||||
"alt-w": "navigation.jump.realtime",
|
||||
"alt-d": "navigation.jump.documentation",
|
||||
"alt-s": "navigation.jump.settings",
|
||||
"ctrl-left": "navigation.jump.back",
|
||||
"ctrl-right": "navigation.jump.forward",
|
||||
}
|
||||
|
||||
/**
|
||||
* A composable that hooks to the caller component's
|
||||
* lifecycle and hooks to the keyboard events to fire
|
||||
* the appropriate actions based on keybindings
|
||||
*/
|
||||
export function hookKeybindingsListener() {
|
||||
onMounted(() => {
|
||||
document.addEventListener("keydown", handleKeyDown)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener("keydown", handleKeyDown)
|
||||
})
|
||||
}
|
||||
|
||||
function handleKeyDown(ev: KeyboardEvent) {
|
||||
// Do not check keybinds if the mode is disabled
|
||||
if (!keybindingsEnabled) return
|
||||
|
||||
const binding = generateKeybindingString(ev)
|
||||
if (!binding) return
|
||||
|
||||
const boundAction = bindings[binding]
|
||||
if (!boundAction) return
|
||||
|
||||
ev.preventDefault()
|
||||
invokeAction(boundAction)
|
||||
}
|
||||
|
||||
function generateKeybindingString(ev: KeyboardEvent): ShortcutKey | null {
|
||||
// All our keybinds need to have one modifier pressed atleast
|
||||
const modifierKey = getActiveModifier(ev)
|
||||
|
||||
const target = ev.target
|
||||
|
||||
if (!modifierKey && !(isDOMElement(target) && isTypableElement(target))) {
|
||||
// Check if we are having singulars instead
|
||||
const key = getPressedKey(ev)
|
||||
|
||||
if (!key) return null
|
||||
else return `${key}` as ShortcutKey
|
||||
}
|
||||
|
||||
const key = getPressedKey(ev)
|
||||
if (!key) return null
|
||||
|
||||
return `${modifierKey}-${key}` as ShortcutKey
|
||||
}
|
||||
|
||||
function getPressedKey(ev: KeyboardEvent): Key | null {
|
||||
const val = ev.key.toLowerCase()
|
||||
|
||||
// Check arrow keys
|
||||
if (val === "arrowup") return "up"
|
||||
else if (val === "arrowdown") return "down"
|
||||
else if (val === "arrowleft") return "left"
|
||||
else if (val === "arrowright") return "right"
|
||||
|
||||
// Check letter keys
|
||||
if (val.length === 1 && val.toUpperCase() !== val.toLowerCase())
|
||||
return val as Key
|
||||
|
||||
// Check if number keys
|
||||
if (val.length === 1 && !isNaN(val as any)) return val as Key
|
||||
|
||||
// Check if question mark
|
||||
if (val === "?") return "?"
|
||||
|
||||
// Check if question mark
|
||||
if (val === "/") return "/"
|
||||
|
||||
// If no other cases match, this is not a valid key
|
||||
return null
|
||||
}
|
||||
|
||||
function getActiveModifier(ev: KeyboardEvent): ModifierKeys | null {
|
||||
const isShiftKey = ev.shiftKey
|
||||
|
||||
// We only allow one modifier key to be pressed (for now)
|
||||
// Control key (+ Command) gets priority and if Alt is also pressed, it is ignored
|
||||
if (isAppleDevice() && ev.metaKey) return isShiftKey ? "ctrl-shift" : "ctrl"
|
||||
else if (!isAppleDevice() && ev.ctrlKey)
|
||||
return isShiftKey ? "ctrl-shift" : "ctrl"
|
||||
|
||||
// Test for Alt key
|
||||
if (ev.altKey) return isShiftKey ? "alt-shift" : "alt"
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* This composable allows for the UI component to be disabled if the component in question is mounted
|
||||
*/
|
||||
export function useKeybindingDisabler() {
|
||||
// TODO: Move to a lock based system that keeps the bindings disabled until all locks are lifted
|
||||
const disableKeybindings = () => {
|
||||
keybindingsEnabled = false
|
||||
}
|
||||
|
||||
const enableKeybindings = () => {
|
||||
keybindingsEnabled = true
|
||||
}
|
||||
|
||||
return {
|
||||
disableKeybindings,
|
||||
enableKeybindings,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { lenses, getSuitableLenses, getLensRenderers } from "../lenses"
|
||||
import rawLens from "../rawLens"
|
||||
|
||||
describe("getSuitableLenses", () => {
|
||||
test("returns raw lens if no content type reported (null/undefined)", () => {
|
||||
const nullResult = getSuitableLenses({
|
||||
headers: {
|
||||
"content-type": null,
|
||||
},
|
||||
})
|
||||
|
||||
const undefinedResult = getSuitableLenses({
|
||||
headers: {},
|
||||
})
|
||||
|
||||
expect(nullResult).toHaveLength(1)
|
||||
expect(nullResult).toContainEqual(rawLens)
|
||||
|
||||
expect(undefinedResult).toHaveLength(1)
|
||||
expect(undefinedResult).toContainEqual(rawLens)
|
||||
})
|
||||
|
||||
const contentTypes = {
|
||||
JSON: [
|
||||
"application/json",
|
||||
"application/ld+json",
|
||||
"application/hal+json; charset=utf8",
|
||||
],
|
||||
Image: [
|
||||
"image/gif",
|
||||
"image/jpeg; foo=bar",
|
||||
"image/png",
|
||||
"image/bmp",
|
||||
"image/svg+xml",
|
||||
"image/x-icon",
|
||||
"image/vnd.microsoft.icon",
|
||||
],
|
||||
HTML: ["text/html", "application/xhtml+xml", "text/html; charset=utf-8"],
|
||||
XML: [
|
||||
"text/xml",
|
||||
"application/xml",
|
||||
"application/xhtml+xml; charset=utf-8",
|
||||
],
|
||||
}
|
||||
|
||||
lenses
|
||||
.filter(({ lensName }) => lensName !== rawLens.lensName)
|
||||
.forEach((el) => {
|
||||
test(`returns ${el.lensName} lens for its content-types`, () => {
|
||||
contentTypes[el.lensName].forEach((contentType) => {
|
||||
expect(
|
||||
getSuitableLenses({
|
||||
headers: {
|
||||
"content-type": contentType,
|
||||
},
|
||||
})
|
||||
).toContainEqual(el)
|
||||
})
|
||||
})
|
||||
|
||||
test(`returns Raw Lens along with ${el.lensName} for the content types`, () => {
|
||||
contentTypes[el.lensName].forEach((contentType) => {
|
||||
expect(
|
||||
getSuitableLenses({
|
||||
headers: {
|
||||
"content-type": contentType,
|
||||
},
|
||||
})
|
||||
).toContainEqual(rawLens)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("getLensRenderers", () => {
|
||||
test("returns all the lens renderers", () => {
|
||||
const res = getLensRenderers()
|
||||
|
||||
lenses.forEach(({ renderer, rendererImport }) => {
|
||||
expect(res).toHaveProperty(renderer)
|
||||
expect(res[renderer]).toBe(rendererImport)
|
||||
})
|
||||
})
|
||||
})
|
||||
10
packages/hoppscotch-app/helpers/lenses/htmlLens.js
Normal file
10
packages/hoppscotch-app/helpers/lenses/htmlLens.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const htmlLens = {
|
||||
lensName: "response.html",
|
||||
isSupportedContentType: (contentType) =>
|
||||
/\btext\/html|application\/xhtml\+xml\b/i.test(contentType),
|
||||
renderer: "htmlres",
|
||||
rendererImport: () =>
|
||||
import("~/components/lenses/renderers/HTMLLensRenderer"),
|
||||
}
|
||||
|
||||
export default htmlLens
|
||||
12
packages/hoppscotch-app/helpers/lenses/imageLens.js
Normal file
12
packages/hoppscotch-app/helpers/lenses/imageLens.js
Normal file
@@ -0,0 +1,12 @@
|
||||
const imageLens = {
|
||||
lensName: "response.image",
|
||||
isSupportedContentType: (contentType) =>
|
||||
/\bimage\/(?:gif|jpeg|png|bmp|svg\+xml|x-icon|vnd\.microsoft\.icon)\b/i.test(
|
||||
contentType
|
||||
),
|
||||
renderer: "imageres",
|
||||
rendererImport: () =>
|
||||
import("~/components/lenses/renderers/ImageLensRenderer"),
|
||||
}
|
||||
|
||||
export default imageLens
|
||||
11
packages/hoppscotch-app/helpers/lenses/jsonLens.js
Normal file
11
packages/hoppscotch-app/helpers/lenses/jsonLens.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { isJSONContentType } from "../utils/contenttypes"
|
||||
|
||||
const jsonLens = {
|
||||
lensName: "response.json",
|
||||
isSupportedContentType: isJSONContentType,
|
||||
renderer: "json",
|
||||
rendererImport: () =>
|
||||
import("~/components/lenses/renderers/JSONLensRenderer"),
|
||||
}
|
||||
|
||||
export default jsonLens
|
||||
28
packages/hoppscotch-app/helpers/lenses/lenses.js
Normal file
28
packages/hoppscotch-app/helpers/lenses/lenses.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import jsonLens from "./jsonLens"
|
||||
import rawLens from "./rawLens"
|
||||
import imageLens from "./imageLens"
|
||||
import htmlLens from "./htmlLens"
|
||||
import xmlLens from "./xmlLens"
|
||||
|
||||
export const lenses = [jsonLens, imageLens, htmlLens, xmlLens, rawLens]
|
||||
|
||||
export function getSuitableLenses(response) {
|
||||
const contentType = response.headers.find((h) => h.key === "content-type")
|
||||
|
||||
if (!contentType) return [rawLens]
|
||||
|
||||
const result = []
|
||||
for (const lens of lenses) {
|
||||
if (lens.isSupportedContentType(contentType.value)) result.push(lens)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
export function getLensRenderers() {
|
||||
const response = {}
|
||||
for (const lens of lenses) {
|
||||
response[lens.renderer] = lens.rendererImport
|
||||
}
|
||||
return response
|
||||
}
|
||||
8
packages/hoppscotch-app/helpers/lenses/rawLens.js
Normal file
8
packages/hoppscotch-app/helpers/lenses/rawLens.js
Normal file
@@ -0,0 +1,8 @@
|
||||
const rawLens = {
|
||||
lensName: "response.raw",
|
||||
isSupportedContentType: () => true,
|
||||
renderer: "raw",
|
||||
rendererImport: () => import("~/components/lenses/renderers/RawLensRenderer"),
|
||||
}
|
||||
|
||||
export default rawLens
|
||||
8
packages/hoppscotch-app/helpers/lenses/xmlLens.js
Normal file
8
packages/hoppscotch-app/helpers/lenses/xmlLens.js
Normal file
@@ -0,0 +1,8 @@
|
||||
const xmlLens = {
|
||||
lensName: "response.xml",
|
||||
isSupportedContentType: (contentType) => /\bxml\b/i.test(contentType),
|
||||
renderer: "xmlres",
|
||||
rendererImport: () => import("~/components/lenses/renderers/XMLLensRenderer"),
|
||||
}
|
||||
|
||||
export default xmlLens
|
||||
15
packages/hoppscotch-app/helpers/migrations.ts
Normal file
15
packages/hoppscotch-app/helpers/migrations.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { settingsStore, applySetting } from "~/newstore/settings"
|
||||
|
||||
/*
|
||||
* This file contains all the migrations we have to perform overtime in various (persisted)
|
||||
* state/store entries
|
||||
*/
|
||||
|
||||
export function performMigrations(): void {
|
||||
// Migrate old default proxy URL to the new proxy URL (if not set / overridden)
|
||||
if (
|
||||
settingsStore.value.PROXY_URL === "https://hoppscotch.apollosoftware.xyz/"
|
||||
) {
|
||||
applySetting("PROXY_URL", "https://proxy.hoppscotch.io/")
|
||||
}
|
||||
}
|
||||
141
packages/hoppscotch-app/helpers/network.ts
Normal file
141
packages/hoppscotch-app/helpers/network.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
import { AxiosRequestConfig } from "axios"
|
||||
import { BehaviorSubject, Observable } from "rxjs"
|
||||
import AxiosStrategy, {
|
||||
cancelRunningAxiosRequest,
|
||||
} from "./strategies/AxiosStrategy"
|
||||
import ExtensionStrategy, {
|
||||
cancelRunningExtensionRequest,
|
||||
hasExtensionInstalled,
|
||||
} from "./strategies/ExtensionStrategy"
|
||||
import { HoppRESTResponse } from "./types/HoppRESTResponse"
|
||||
import { EffectiveHoppRESTRequest } from "./utils/EffectiveURL"
|
||||
import { settingsStore } from "~/newstore/settings"
|
||||
|
||||
export const cancelRunningRequest = () => {
|
||||
if (isExtensionsAllowed() && hasExtensionInstalled()) {
|
||||
cancelRunningExtensionRequest()
|
||||
} else {
|
||||
cancelRunningAxiosRequest()
|
||||
}
|
||||
}
|
||||
|
||||
const isExtensionsAllowed = () => settingsStore.value.EXTENSIONS_ENABLED
|
||||
|
||||
const runAppropriateStrategy = (req: AxiosRequestConfig) => {
|
||||
if (isExtensionsAllowed() && hasExtensionInstalled()) {
|
||||
return ExtensionStrategy(req)
|
||||
}
|
||||
|
||||
return AxiosStrategy(req)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an identifier for how a request will be ran
|
||||
* if the system is asked to fire a request
|
||||
*
|
||||
* @returns {"normal" | "extension" | "proxy"}
|
||||
*/
|
||||
export function getCurrentStrategyID() {
|
||||
if (isExtensionsAllowed() && hasExtensionInstalled()) {
|
||||
return "extension"
|
||||
} else if (settingsStore.value.PROXY_ENABLED) {
|
||||
return "proxy"
|
||||
} else {
|
||||
return "normal"
|
||||
}
|
||||
}
|
||||
|
||||
export const sendNetworkRequest = (req: any) =>
|
||||
runAppropriateStrategy(req).finally(() => window.$nuxt.$loading.finish())
|
||||
|
||||
export function createRESTNetworkRequestStream(
|
||||
req: EffectiveHoppRESTRequest
|
||||
): Observable<HoppRESTResponse> {
|
||||
const response = new BehaviorSubject<HoppRESTResponse>({
|
||||
type: "loading",
|
||||
req,
|
||||
})
|
||||
|
||||
const headers = req.effectiveFinalHeaders.reduce((acc, { key, value }) => {
|
||||
return Object.assign(acc, { [key]: value })
|
||||
}, {})
|
||||
|
||||
const params = req.effectiveFinalParams.reduce((acc, { key, value }) => {
|
||||
return Object.assign(acc, { [key]: value })
|
||||
}, {})
|
||||
|
||||
const timeStart = Date.now()
|
||||
|
||||
runAppropriateStrategy({
|
||||
method: req.method as any,
|
||||
url: req.effectiveFinalURL,
|
||||
headers,
|
||||
params,
|
||||
data: req.effectiveFinalBody,
|
||||
})
|
||||
.then((res: any) => {
|
||||
const timeEnd = Date.now()
|
||||
|
||||
const contentLength = res.headers["content-length"]
|
||||
? parseInt(res.headers["content-length"])
|
||||
: (res.data as ArrayBuffer).byteLength
|
||||
|
||||
const resObj: HoppRESTResponse = {
|
||||
type: "success",
|
||||
statusCode: res.status,
|
||||
body: res.data,
|
||||
headers: Object.keys(res.headers).map((x) => ({
|
||||
key: x,
|
||||
value: res.headers[x],
|
||||
})),
|
||||
meta: {
|
||||
responseSize: contentLength,
|
||||
responseDuration: timeEnd - timeStart,
|
||||
},
|
||||
req,
|
||||
}
|
||||
response.next(resObj)
|
||||
|
||||
response.complete()
|
||||
})
|
||||
.catch((e) => {
|
||||
if (e.response) {
|
||||
const timeEnd = Date.now()
|
||||
|
||||
const contentLength = e.response.headers["content-length"]
|
||||
? parseInt(e.response.headers["content-length"])
|
||||
: (e.response.data as ArrayBuffer).byteLength
|
||||
|
||||
const resObj: HoppRESTResponse = {
|
||||
type: "fail",
|
||||
body: e.response.data,
|
||||
headers: Object.keys(e.response.headers).map((x) => ({
|
||||
key: x,
|
||||
value: e.response.headers[x],
|
||||
})),
|
||||
meta: {
|
||||
responseDuration: timeEnd - timeStart,
|
||||
responseSize: contentLength,
|
||||
},
|
||||
req,
|
||||
statusCode: e.response.status,
|
||||
}
|
||||
|
||||
response.next(resObj)
|
||||
|
||||
response.complete()
|
||||
} else {
|
||||
const resObj: HoppRESTResponse = {
|
||||
type: "network_fail",
|
||||
error: e,
|
||||
req,
|
||||
}
|
||||
|
||||
response.next(resObj)
|
||||
|
||||
response.complete()
|
||||
}
|
||||
})
|
||||
|
||||
return response
|
||||
}
|
||||
242
packages/hoppscotch-app/helpers/oauth.js
Normal file
242
packages/hoppscotch-app/helpers/oauth.js
Normal file
@@ -0,0 +1,242 @@
|
||||
import {
|
||||
getLocalConfig,
|
||||
setLocalConfig,
|
||||
removeLocalConfig,
|
||||
} from "~/newstore/localpersistence"
|
||||
|
||||
const redirectUri = `${window.location.origin}/`
|
||||
|
||||
// GENERAL HELPER FUNCTIONS
|
||||
|
||||
/**
|
||||
* Makes a POST request and parse the response as JSON
|
||||
*
|
||||
* @param {String} url - The resource
|
||||
* @param {Object} params - Configuration options
|
||||
* @returns {Object}
|
||||
*/
|
||||
|
||||
const sendPostRequest = async (url, params) => {
|
||||
const body = Object.keys(params)
|
||||
.map((key) => `${key}=${params[key]}`)
|
||||
.join("&")
|
||||
const options = {
|
||||
method: "post",
|
||||
headers: {
|
||||
"Content-type": "application/x-www-form-urlencoded; charset=UTF-8",
|
||||
},
|
||||
body,
|
||||
}
|
||||
try {
|
||||
const response = await fetch(url, options)
|
||||
const data = await response.json()
|
||||
return data
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a query string into an object
|
||||
*
|
||||
* @param {String} searchQuery - The search query params
|
||||
* @returns {Object}
|
||||
*/
|
||||
|
||||
const parseQueryString = (searchQuery) => {
|
||||
if (searchQuery === "") {
|
||||
return {}
|
||||
}
|
||||
const segments = searchQuery.split("&").map((s) => s.split("="))
|
||||
const queryString = segments.reduce(
|
||||
(obj, el) => ({ ...obj, [el[0]]: el[1] }),
|
||||
{}
|
||||
)
|
||||
return queryString
|
||||
}
|
||||
|
||||
/**
|
||||
* Get OAuth configuration from OpenID Discovery endpoint
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
|
||||
const getTokenConfiguration = async (endpoint) => {
|
||||
const options = {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
},
|
||||
}
|
||||
try {
|
||||
const response = await fetch(endpoint, options)
|
||||
const config = await response.json()
|
||||
return config
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
// PKCE HELPER FUNCTIONS
|
||||
|
||||
/**
|
||||
* Generates a secure random string using the browser crypto functions
|
||||
*
|
||||
* @returns {Object}
|
||||
*/
|
||||
|
||||
const generateRandomString = () => {
|
||||
const array = new Uint32Array(28)
|
||||
window.crypto.getRandomValues(array)
|
||||
return Array.from(array, (dec) => `0${dec.toString(16)}`.substr(-2)).join("")
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the SHA256 hash of the input text
|
||||
*
|
||||
* @returns {Promise<ArrayBuffer>}
|
||||
*/
|
||||
|
||||
const sha256 = (plain) => {
|
||||
const encoder = new TextEncoder()
|
||||
const data = encoder.encode(plain)
|
||||
return window.crypto.subtle.digest("SHA-256", data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes the input string into Base64 format
|
||||
*
|
||||
* @param {String} str - The string to be converted
|
||||
* @returns {Promise<ArrayBuffer>}
|
||||
*/
|
||||
|
||||
const base64urlencode = (
|
||||
str // Converts the ArrayBuffer to string using Uint8 array to convert to what btoa accepts.
|
||||
) =>
|
||||
// btoa accepts chars only within ascii 0-255 and base64 encodes them.
|
||||
// Then convert the base64 encoded to base64url encoded
|
||||
// (replace + with -, replace / with _, trim trailing =)
|
||||
btoa(String.fromCharCode.apply(null, new Uint8Array(str)))
|
||||
.replace(/\+/g, "-")
|
||||
.replace(/\//g, "_")
|
||||
.replace(/=+$/, "")
|
||||
|
||||
/**
|
||||
* Return the base64-urlencoded sha256 hash for the PKCE challenge
|
||||
*
|
||||
* @param {String} v - The randomly generated string
|
||||
* @returns {String}
|
||||
*/
|
||||
|
||||
const pkceChallengeFromVerifier = async (v) => {
|
||||
const hashed = await sha256(v)
|
||||
return base64urlencode(hashed)
|
||||
}
|
||||
|
||||
// OAUTH REQUEST
|
||||
|
||||
/**
|
||||
* Initiates PKCE Auth Code flow when requested
|
||||
*
|
||||
* @param {Object} - The necessary params
|
||||
* @returns {Void}
|
||||
*/
|
||||
|
||||
const tokenRequest = async ({
|
||||
oidcDiscoveryUrl,
|
||||
grantType,
|
||||
authUrl,
|
||||
accessTokenUrl,
|
||||
clientId,
|
||||
scope,
|
||||
}) => {
|
||||
// Check oauth configuration
|
||||
if (oidcDiscoveryUrl !== "") {
|
||||
// eslint-disable-next-line camelcase
|
||||
const { authorization_endpoint, token_endpoint } =
|
||||
await getTokenConfiguration(oidcDiscoveryUrl)
|
||||
// eslint-disable-next-line camelcase
|
||||
authUrl = authorization_endpoint
|
||||
// eslint-disable-next-line camelcase
|
||||
accessTokenUrl = token_endpoint
|
||||
}
|
||||
|
||||
// Store oauth information
|
||||
setLocalConfig("tokenEndpoint", accessTokenUrl)
|
||||
setLocalConfig("client_id", clientId)
|
||||
|
||||
// Create and store a random state value
|
||||
const state = generateRandomString()
|
||||
setLocalConfig("pkce_state", state)
|
||||
|
||||
// Create and store a new PKCE codeVerifier (the plaintext random secret)
|
||||
const codeVerifier = generateRandomString()
|
||||
setLocalConfig("pkce_codeVerifier", codeVerifier)
|
||||
|
||||
// Hash and base64-urlencode the secret to use as the challenge
|
||||
const codeChallenge = await pkceChallengeFromVerifier(codeVerifier)
|
||||
|
||||
// Build the authorization URL
|
||||
const buildUrl = () =>
|
||||
`${authUrl + `?response_type=${grantType}`}&client_id=${encodeURIComponent(
|
||||
clientId
|
||||
)}&state=${encodeURIComponent(state)}&scope=${encodeURIComponent(
|
||||
scope
|
||||
)}&redirect_uri=${encodeURIComponent(
|
||||
redirectUri
|
||||
)}&code_challenge=${encodeURIComponent(
|
||||
codeChallenge
|
||||
)}&code_challenge_method=S256`
|
||||
|
||||
// Redirect to the authorization server
|
||||
window.location = buildUrl()
|
||||
}
|
||||
|
||||
// OAUTH REDIRECT HANDLING
|
||||
|
||||
/**
|
||||
* Handle the redirect back from the authorization server and
|
||||
* get an access token from the token endpoint
|
||||
*
|
||||
* @returns {Promise<any | void>}
|
||||
*/
|
||||
|
||||
const oauthRedirect = () => {
|
||||
let tokenResponse = ""
|
||||
const q = parseQueryString(window.location.search.substring(1))
|
||||
// Check if the server returned an error string
|
||||
if (q.error) {
|
||||
alert(`Error returned from authorization server: ${q.error}`)
|
||||
}
|
||||
// If the server returned an authorization code, attempt to exchange it for an access token
|
||||
if (q.code) {
|
||||
// Verify state matches what we set at the beginning
|
||||
if (getLocalConfig("pkce_state") !== q.state) {
|
||||
alert("Invalid state")
|
||||
Promise.reject(tokenResponse)
|
||||
} else {
|
||||
try {
|
||||
// Exchange the authorization code for an access token
|
||||
tokenResponse = sendPostRequest(getLocalConfig("tokenEndpoint"), {
|
||||
grant_type: "authorization_code",
|
||||
code: q.code,
|
||||
client_id: getLocalConfig("client_id"),
|
||||
redirect_uri: redirectUri,
|
||||
code_verifier: getLocalConfig("pkce_codeVerifier"),
|
||||
})
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
return Promise.reject(tokenResponse)
|
||||
}
|
||||
}
|
||||
// Clean these up since we don't need them anymore
|
||||
removeLocalConfig("pkce_state")
|
||||
removeLocalConfig("pkce_codeVerifier")
|
||||
removeLocalConfig("tokenEndpoint")
|
||||
removeLocalConfig("client_id")
|
||||
return tokenResponse
|
||||
}
|
||||
return Promise.reject(tokenResponse)
|
||||
}
|
||||
|
||||
export { tokenRequest, oauthRedirect }
|
||||
124
packages/hoppscotch-app/helpers/outline.js
Normal file
124
packages/hoppscotch-app/helpers/outline.js
Normal file
@@ -0,0 +1,124 @@
|
||||
import jsonParse from "./jsonParse"
|
||||
|
||||
export default () => {
|
||||
let jsonAST = {}
|
||||
let path = []
|
||||
|
||||
const init = (jsonStr) => {
|
||||
jsonAST = jsonParse(jsonStr)
|
||||
linkParents(jsonAST)
|
||||
}
|
||||
|
||||
const setNewText = (jsonStr) => {
|
||||
init(jsonStr)
|
||||
path = []
|
||||
}
|
||||
|
||||
const linkParents = (node) => {
|
||||
if (node.kind === "Object") {
|
||||
if (node.members) {
|
||||
node.members.forEach((m) => {
|
||||
m.parent = node
|
||||
linkParents(m)
|
||||
})
|
||||
}
|
||||
} else if (node.kind === "Array") {
|
||||
if (node.values) {
|
||||
node.values.forEach((v) => {
|
||||
v.parent = node
|
||||
linkParents(v)
|
||||
})
|
||||
}
|
||||
} else if (node.kind === "Member") {
|
||||
if (node.value) {
|
||||
node.value.parent = node
|
||||
linkParents(node.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const genPath = (index) => {
|
||||
let output = {}
|
||||
path = []
|
||||
let current = jsonAST
|
||||
if (current.kind === "Object") {
|
||||
path.push({ label: "{}", obj: "root" })
|
||||
} else if (current.kind === "Array") {
|
||||
path.push({ label: "[]", obj: "root" })
|
||||
}
|
||||
let over = false
|
||||
|
||||
try {
|
||||
while (!over) {
|
||||
if (current.kind === "Object") {
|
||||
let i = 0
|
||||
let found = false
|
||||
while (i < current.members.length) {
|
||||
const m = current.members[i]
|
||||
if (m.start <= index && m.end >= index) {
|
||||
path.push({ label: m.key.value, obj: m })
|
||||
current = current.members[i]
|
||||
found = true
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
if (!found) over = true
|
||||
} else if (current.kind === "Array") {
|
||||
if (current.values) {
|
||||
let i = 0
|
||||
let found = false
|
||||
while (i < current.values.length) {
|
||||
const m = current.values[i]
|
||||
if (m.start <= index && m.end >= index) {
|
||||
path.push({ label: `[${i.toString()}]`, obj: m })
|
||||
current = current.values[i]
|
||||
found = true
|
||||
break
|
||||
}
|
||||
i++
|
||||
}
|
||||
if (!found) over = true
|
||||
} else over = true
|
||||
} else if (current.kind === "Member") {
|
||||
if (current.value) {
|
||||
if (current.value.start <= index && current.value.end >= index) {
|
||||
current = current.value
|
||||
} else over = true
|
||||
} else over = true
|
||||
} else if (
|
||||
current.kind === "String" ||
|
||||
current.kind === "Number" ||
|
||||
current.kind === "Boolean" ||
|
||||
current.kind === "Null"
|
||||
) {
|
||||
if (current.start <= index && current.end >= index) {
|
||||
path.push({ label: `${current.value}`, obj: current })
|
||||
}
|
||||
over = true
|
||||
}
|
||||
}
|
||||
output = { success: true, res: path.map((p) => p.label) }
|
||||
} catch (e) {
|
||||
output = { success: false, res: e }
|
||||
}
|
||||
return output
|
||||
}
|
||||
|
||||
const getSiblings = (index) => {
|
||||
const parent = path[index]?.obj?.parent
|
||||
if (!parent) return []
|
||||
else if (parent.kind === "Object") {
|
||||
return parent.members
|
||||
} else if (parent.kind === "Array") {
|
||||
return parent.values
|
||||
} else return []
|
||||
}
|
||||
|
||||
return {
|
||||
init,
|
||||
genPath,
|
||||
getSiblings,
|
||||
setNewText,
|
||||
}
|
||||
}
|
||||
11
packages/hoppscotch-app/helpers/platformutils.js
Normal file
11
packages/hoppscotch-app/helpers/platformutils.js
Normal file
@@ -0,0 +1,11 @@
|
||||
export function isAppleDevice() {
|
||||
return /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform)
|
||||
}
|
||||
|
||||
export function getPlatformSpecialKey() {
|
||||
return isAppleDevice() ? "⌘" : "Ctrl"
|
||||
}
|
||||
|
||||
export function getPlatformAlternateKey() {
|
||||
return isAppleDevice() ? "⌥" : "Alt"
|
||||
}
|
||||
321
packages/hoppscotch-app/helpers/postwomanTesting.ts
Normal file
321
packages/hoppscotch-app/helpers/postwomanTesting.ts
Normal file
@@ -0,0 +1,321 @@
|
||||
import { HoppRESTResponse } from "./types/HoppRESTResponse"
|
||||
|
||||
const styles = {
|
||||
PASS: { icon: "check", class: "success-response" },
|
||||
FAIL: { icon: "close", class: "cl-error-response" },
|
||||
ERROR: { icon: "close", class: "cl-error-response" },
|
||||
}
|
||||
|
||||
type TestScriptResponse = {
|
||||
body: any
|
||||
headers: any[]
|
||||
status: number
|
||||
|
||||
__newRes: HoppRESTResponse
|
||||
}
|
||||
|
||||
type TestScriptVariables = {
|
||||
response: TestScriptResponse
|
||||
}
|
||||
|
||||
type TestReportStartBlock = {
|
||||
startBlock: string
|
||||
}
|
||||
|
||||
type TestReportEndBlock = {
|
||||
endBlock: true
|
||||
}
|
||||
|
||||
type TestReportEntry = {
|
||||
result: "PASS" | "FAIL" | "ERROR"
|
||||
message: string
|
||||
styles: {
|
||||
icon: string
|
||||
}
|
||||
}
|
||||
|
||||
type TestReport = TestReportStartBlock | TestReportEntry | TestReportEndBlock
|
||||
|
||||
export default function runTestScriptWithVariables(
|
||||
script: string,
|
||||
variables?: TestScriptVariables
|
||||
) {
|
||||
const pw = {
|
||||
_errors: [],
|
||||
_testReports: [] as TestReport[],
|
||||
_report: "",
|
||||
expect(value: any) {
|
||||
try {
|
||||
return expect(value, this._testReports)
|
||||
} catch (e) {
|
||||
pw._testReports.push({
|
||||
result: "ERROR",
|
||||
message: e.toString(),
|
||||
styles: styles.ERROR,
|
||||
})
|
||||
}
|
||||
},
|
||||
test: (descriptor: string, func: () => void) =>
|
||||
test(descriptor, func, pw._testReports),
|
||||
// globals that the script is allowed to have access to.
|
||||
}
|
||||
Object.assign(pw, variables)
|
||||
|
||||
// run pre-request script within this function so that it has access to the pw object.
|
||||
// eslint-disable-next-line no-new-func
|
||||
new Function("pw", script)(pw)
|
||||
|
||||
return {
|
||||
report: pw._report,
|
||||
errors: pw._errors,
|
||||
testResults: pw._testReports,
|
||||
}
|
||||
}
|
||||
|
||||
function test(
|
||||
descriptor: string,
|
||||
func: () => void,
|
||||
_testReports: TestReport[]
|
||||
) {
|
||||
_testReports.push({ startBlock: descriptor })
|
||||
try {
|
||||
func()
|
||||
} catch (e) {
|
||||
_testReports.push({ result: "ERROR", message: e, styles: styles.ERROR })
|
||||
}
|
||||
_testReports.push({ endBlock: true })
|
||||
|
||||
// TODO: Organize and generate text report of each {descriptor: true} section in testReports.
|
||||
// add checkmark or x depending on if each testReport is pass=true or pass=false
|
||||
}
|
||||
|
||||
function expect(expectValue: any, _testReports: TestReport[]) {
|
||||
return new Expectation(expectValue, null, _testReports)
|
||||
}
|
||||
|
||||
class Expectation {
|
||||
private expectValue: any
|
||||
private not: true | Expectation
|
||||
private _testReports: TestReport[]
|
||||
|
||||
constructor(
|
||||
expectValue: any,
|
||||
_not: boolean | null,
|
||||
_testReports: TestReport[]
|
||||
) {
|
||||
this.expectValue = expectValue
|
||||
this.not = _not || new Expectation(this.expectValue, true, _testReports)
|
||||
this._testReports = _testReports // this values is used within Test.it, which wraps Expectation and passes _testReports value.
|
||||
}
|
||||
|
||||
private _satisfies(expectValue: any, targetValue?: any): boolean {
|
||||
// Used for testing if two values match the expectation, which could be === OR !==, depending on if not
|
||||
// was used. Expectation#_satisfies prevents the need to have an if(this.not) branch in every test method.
|
||||
// Signature is _satisfies([expectValue,] targetValue): if only one argument is given, it is assumed the targetValue, and expectValue is set to this.expectValue
|
||||
if (!targetValue) {
|
||||
targetValue = expectValue
|
||||
expectValue = this.expectValue
|
||||
}
|
||||
if (this.not === true) {
|
||||
// test the inverse. this.not is always truthly, but an Expectation that is inverted will always be strictly `true`
|
||||
return expectValue !== targetValue
|
||||
} else {
|
||||
return expectValue === targetValue
|
||||
}
|
||||
}
|
||||
|
||||
_fmtNot(message: string) {
|
||||
// given a string with "(not)" in it, replaces with "not" or "", depending if the expectation is expecting the positive or inverse (this._not)
|
||||
if (this.not === true) {
|
||||
return message.replace("(not)", "not ")
|
||||
} else {
|
||||
return message.replace("(not)", "")
|
||||
}
|
||||
}
|
||||
|
||||
_fail(message: string) {
|
||||
return this._testReports.push({
|
||||
result: "FAIL",
|
||||
message,
|
||||
styles: styles.FAIL,
|
||||
})
|
||||
}
|
||||
|
||||
_pass(message: string) {
|
||||
return this._testReports.push({
|
||||
result: "PASS",
|
||||
message,
|
||||
styles: styles.PASS,
|
||||
})
|
||||
}
|
||||
|
||||
// TEST METHODS DEFINED BELOW
|
||||
// these are the usual methods that would follow expect(...)
|
||||
toBe(value: any) {
|
||||
return this._satisfies(value)
|
||||
? this._pass(
|
||||
this._fmtNot(`${this.expectValue} do (not)match with ${value}`)
|
||||
)
|
||||
: this._fail(
|
||||
this._fmtNot(`Expected ${this.expectValue} (not)to be ${value}`)
|
||||
)
|
||||
}
|
||||
|
||||
toHaveProperty(value: string) {
|
||||
return this._satisfies(
|
||||
Object.prototype.hasOwnProperty.call(this.expectValue, value),
|
||||
true
|
||||
)
|
||||
? this._pass(
|
||||
this._fmtNot(`${this.expectValue} do (not)have property ${value}`)
|
||||
)
|
||||
: this._fail(
|
||||
this._fmtNot(
|
||||
`Expected object ${this.expectValue} to (not)have property ${value}`
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
toBeLevel2xx() {
|
||||
const code = parseInt(this.expectValue, 10)
|
||||
if (Number.isNaN(code)) {
|
||||
return this._fail(
|
||||
`Expected 200-level status but could not parse value ${this.expectValue}`
|
||||
)
|
||||
}
|
||||
return this._satisfies(code >= 200 && code < 300, true)
|
||||
? this._pass(
|
||||
this._fmtNot(`${this.expectValue} is (not)a 200-level status`)
|
||||
)
|
||||
: this._fail(
|
||||
this._fmtNot(
|
||||
`Expected ${this.expectValue} to (not)be 200-level status`
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
toBeLevel3xx() {
|
||||
const code = parseInt(this.expectValue, 10)
|
||||
if (Number.isNaN(code)) {
|
||||
return this._fail(
|
||||
`Expected 300-level status but could not parse value ${this.expectValue}`
|
||||
)
|
||||
}
|
||||
return this._satisfies(code >= 300 && code < 400, true)
|
||||
? this._pass(
|
||||
this._fmtNot(`${this.expectValue} is (not)a 300-level status`)
|
||||
)
|
||||
: this._fail(
|
||||
this._fmtNot(
|
||||
`Expected ${this.expectValue} to (not)be 300-level status`
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
toBeLevel4xx() {
|
||||
const code = parseInt(this.expectValue, 10)
|
||||
if (Number.isNaN(code)) {
|
||||
return this._fail(
|
||||
`Expected 400-level status but could not parse value ${this.expectValue}`
|
||||
)
|
||||
}
|
||||
return this._satisfies(code >= 400 && code < 500, true)
|
||||
? this._pass(
|
||||
this._fmtNot(`${this.expectValue} is (not)a 400-level status`)
|
||||
)
|
||||
: this._fail(
|
||||
this._fmtNot(
|
||||
`Expected ${this.expectValue} to (not)be 400-level status`
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
toBeLevel5xx() {
|
||||
const code = parseInt(this.expectValue, 10)
|
||||
if (Number.isNaN(code)) {
|
||||
return this._fail(
|
||||
`Expected 500-level status but could not parse value ${this.expectValue}`
|
||||
)
|
||||
}
|
||||
return this._satisfies(code >= 500 && code < 600, true)
|
||||
? this._pass(
|
||||
this._fmtNot(`${this.expectValue} is (not)a 500-level status`)
|
||||
)
|
||||
: this._fail(
|
||||
this._fmtNot(
|
||||
`Expected ${this.expectValue} to (not)be 500-level status`
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
toHaveLength(expectedLength: number) {
|
||||
const actualLength = this.expectValue.length
|
||||
return this._satisfies(actualLength, expectedLength)
|
||||
? this._pass(
|
||||
this._fmtNot(
|
||||
`Length expectation of (not)being ${expectedLength} is kept`
|
||||
)
|
||||
)
|
||||
: this._fail(
|
||||
this._fmtNot(
|
||||
`Expected length to be ${expectedLength} but actual length was ${actualLength}`
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
toBeType(expectedType: string) {
|
||||
const actualType = typeof this.expectValue
|
||||
if (
|
||||
![
|
||||
"string",
|
||||
"boolean",
|
||||
"number",
|
||||
"object",
|
||||
"undefined",
|
||||
"bigint",
|
||||
"symbol",
|
||||
"function",
|
||||
].includes(expectedType)
|
||||
) {
|
||||
return this._fail(
|
||||
this._fmtNot(
|
||||
`Argument for toBeType should be "string", "boolean", "number", "object", "undefined", "bigint", "symbol" or "function"`
|
||||
)
|
||||
)
|
||||
}
|
||||
return this._satisfies(actualType, expectedType)
|
||||
? this._pass(this._fmtNot(`The type is (not)"${expectedType}"`))
|
||||
: this._fail(
|
||||
this._fmtNot(
|
||||
`Expected type to be "${expectedType}" but actual type was "${actualType}"`
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function transformResponseForTesting(
|
||||
response: HoppRESTResponse
|
||||
): TestScriptResponse {
|
||||
if (response.type === "loading") {
|
||||
throw new Error("Cannot transform loading responses")
|
||||
}
|
||||
|
||||
if (response.type === "network_fail") {
|
||||
throw new Error("Cannot transform failed responses")
|
||||
}
|
||||
|
||||
let body: any = new TextDecoder("utf-8").decode(response.body)
|
||||
|
||||
// Try parsing to JSON
|
||||
try {
|
||||
body = JSON.parse(body)
|
||||
} catch (_) {}
|
||||
|
||||
return {
|
||||
body,
|
||||
headers: response.headers,
|
||||
status: response.statusCode,
|
||||
|
||||
__newRes: response,
|
||||
}
|
||||
}
|
||||
42
packages/hoppscotch-app/helpers/preRequest.ts
Normal file
42
packages/hoppscotch-app/helpers/preRequest.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import {
|
||||
getCurrentEnvironment,
|
||||
getGlobalVariables,
|
||||
} from "~/newstore/environments"
|
||||
|
||||
export default function getEnvironmentVariablesFromScript(script: string) {
|
||||
const _variables: Record<string, string> = {}
|
||||
|
||||
const currentEnv = getCurrentEnvironment()
|
||||
|
||||
for (const variable of currentEnv.variables) {
|
||||
_variables[variable.key] = variable.value
|
||||
}
|
||||
|
||||
const globalEnv = getGlobalVariables()
|
||||
|
||||
if (globalEnv) {
|
||||
for (const variable of globalEnv) {
|
||||
_variables[variable.key] = variable.value
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// the pw object is the proxy by which pre-request scripts can pass variables to the request.
|
||||
// for security and control purposes, this is the only way a pre-request script should modify variables.
|
||||
const pw = {
|
||||
environment: {
|
||||
set: (key: string, value: string) => (_variables[key] = value),
|
||||
},
|
||||
env: {
|
||||
set: (key: string, value: string) => (_variables[key] = value),
|
||||
},
|
||||
// globals that the script is allowed to have access to.
|
||||
}
|
||||
|
||||
// run pre-request script within this function so that it has access to the pw object.
|
||||
// eslint-disable-next-line no-new-func
|
||||
new Function("pw", script)(pw)
|
||||
} catch (_e) {}
|
||||
|
||||
return _variables
|
||||
}
|
||||
21
packages/hoppscotch-app/helpers/preRequestScriptSnippets.js
Normal file
21
packages/hoppscotch-app/helpers/preRequestScriptSnippets.js
Normal file
@@ -0,0 +1,21 @@
|
||||
export default [
|
||||
{
|
||||
name: "Environment: Set an environment variable",
|
||||
script: `\n\n// Set an environment variable
|
||||
pw.env.set("variable", "value");`,
|
||||
},
|
||||
{
|
||||
name: "Environment: Set timestamp variable",
|
||||
script: `\n\n// Set timestamp variable
|
||||
const cuttentTime = Date.now();
|
||||
pw.env.set("timestamp", cuttentTime.toString());`,
|
||||
},
|
||||
{
|
||||
name: "Environment: Set random number variable",
|
||||
script: `\n\n// Set random number variable
|
||||
const min = 1
|
||||
const max = 1000
|
||||
const randomArbitrary = Math.random() * (max - min) + min
|
||||
pw.env.set("randomNumber", randomArbitrary.toString());`,
|
||||
},
|
||||
]
|
||||
58
packages/hoppscotch-app/helpers/pwa.js
Normal file
58
packages/hoppscotch-app/helpers/pwa.js
Normal file
@@ -0,0 +1,58 @@
|
||||
import { getLocalConfig, setLocalConfig } from "~/newstore/localpersistence"
|
||||
|
||||
export default () => {
|
||||
//* ** Determine whether or not the PWA has been installed. ***//
|
||||
|
||||
// Step 1: Check local storage
|
||||
let pwaInstalled = getLocalConfig("pwaInstalled") === "yes"
|
||||
|
||||
// Step 2: Check if the display-mode is standalone. (Only permitted for PWAs.)
|
||||
if (
|
||||
!pwaInstalled &&
|
||||
window.matchMedia("(display-mode: standalone)").matches
|
||||
) {
|
||||
setLocalConfig("pwaInstalled", "yes")
|
||||
pwaInstalled = true
|
||||
}
|
||||
|
||||
// Step 3: Check if the navigator is in standalone mode. (Again, only permitted for PWAs.)
|
||||
if (!pwaInstalled && window.navigator.standalone === true) {
|
||||
setLocalConfig("pwaInstalled", "yes")
|
||||
pwaInstalled = true
|
||||
}
|
||||
|
||||
//* ** If the PWA has not been installed, show the install PWA prompt.. ***//
|
||||
let deferredPrompt = null
|
||||
window.addEventListener("beforeinstallprompt", (event) => {
|
||||
deferredPrompt = event
|
||||
|
||||
// Show the install button if the prompt appeared.
|
||||
if (!pwaInstalled) {
|
||||
document.querySelector("#installPWA").style.display = "inline-flex"
|
||||
}
|
||||
})
|
||||
|
||||
// When the app is installed, remove install prompts.
|
||||
window.addEventListener("appinstalled", () => {
|
||||
setLocalConfig("pwaInstalled", "yes")
|
||||
pwaInstalled = true
|
||||
document.getElementById("installPWA").style.display = "none"
|
||||
})
|
||||
|
||||
// When the app is uninstalled, add the prompts back
|
||||
return async () => {
|
||||
if (deferredPrompt) {
|
||||
deferredPrompt.prompt()
|
||||
const outcome = await deferredPrompt.userChoice
|
||||
|
||||
if (outcome === "accepted") {
|
||||
console.log("Hoppscotch was installed successfully.")
|
||||
} else {
|
||||
console.log(
|
||||
"Hoppscotch could not be installed. (Installation rejected by user.)"
|
||||
)
|
||||
}
|
||||
deferredPrompt = null
|
||||
}
|
||||
}
|
||||
}
|
||||
33
packages/hoppscotch-app/helpers/requestParams.js
Normal file
33
packages/hoppscotch-app/helpers/requestParams.js
Normal file
@@ -0,0 +1,33 @@
|
||||
export function hasPathParams(params) {
|
||||
return params
|
||||
.filter((item) =>
|
||||
Object.prototype.hasOwnProperty.call(item, "active")
|
||||
? item.active === true
|
||||
: true
|
||||
)
|
||||
.some(({ type }) => type === "path")
|
||||
}
|
||||
|
||||
export function addPathParamsToVariables(params, variables) {
|
||||
params
|
||||
.filter((item) =>
|
||||
Object.prototype.hasOwnProperty.call(item, "active")
|
||||
? item.active === true
|
||||
: true
|
||||
)
|
||||
.filter(({ key }) => !!key)
|
||||
.filter(({ type }) => type === "path")
|
||||
.forEach(({ key, value }) => (variables[key] = value))
|
||||
return variables
|
||||
}
|
||||
|
||||
export function getQueryParams(params) {
|
||||
return params
|
||||
.filter((item) =>
|
||||
Object.prototype.hasOwnProperty.call(item, "active")
|
||||
? item.active === true
|
||||
: true
|
||||
)
|
||||
.filter(({ key }) => !!key)
|
||||
.filter(({ type }) => type !== "path")
|
||||
}
|
||||
292
packages/hoppscotch-app/helpers/shortcuts.js
Normal file
292
packages/hoppscotch-app/helpers/shortcuts.js
Normal file
@@ -0,0 +1,292 @@
|
||||
import { getPlatformAlternateKey, getPlatformSpecialKey } from "./platformutils"
|
||||
|
||||
export default [
|
||||
{
|
||||
section: "shortcut.general.title",
|
||||
shortcuts: [
|
||||
{
|
||||
keys: ["?"],
|
||||
label: "shortcut.general.help_menu",
|
||||
},
|
||||
{
|
||||
keys: ["/"],
|
||||
label: "shortcut.general.command_menu",
|
||||
},
|
||||
{
|
||||
keys: [getPlatformSpecialKey(), "K"],
|
||||
label: "shortcut.general.show_all",
|
||||
},
|
||||
{
|
||||
keys: ["Esc"],
|
||||
label: "shortcut.general.close_current_menu",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
section: "shortcut.request.title",
|
||||
shortcuts: [
|
||||
{
|
||||
keys: [getPlatformSpecialKey(), "G"],
|
||||
label: "shortcut.request.send_request",
|
||||
},
|
||||
{
|
||||
keys: [getPlatformSpecialKey(), "S"],
|
||||
label: "shortcut.request.save_to_collections",
|
||||
},
|
||||
{
|
||||
keys: [getPlatformSpecialKey(), "U"],
|
||||
label: "shortcut.request.copy_request_link",
|
||||
},
|
||||
{
|
||||
keys: [getPlatformSpecialKey(), "I"],
|
||||
label: "shortcut.request.reset_request",
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "↑"],
|
||||
label: "shortcut.request.next_method",
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "↓"],
|
||||
label: "shortcut.request.previous_method",
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "G"],
|
||||
label: "shortcut.request.get_method",
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "H"],
|
||||
label: "shortcut.request.head_method",
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "P"],
|
||||
label: "shortcut.request.post_method",
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "U"],
|
||||
label: "shortcut.request.put_method",
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "X"],
|
||||
label: "shortcut.request.delete_method",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
section: "shortcut.navigation.title",
|
||||
shortcuts: [
|
||||
{
|
||||
keys: [getPlatformSpecialKey(), "←"],
|
||||
label: "shortcut.navigation.back",
|
||||
},
|
||||
{
|
||||
keys: [getPlatformSpecialKey(), "→"],
|
||||
label: "shortcut.navigation.forward",
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "R"],
|
||||
label: "shortcut.navigation.rest",
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "Q"],
|
||||
label: "shortcut.navigation.graphql",
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "W"],
|
||||
label: "shortcut.navigation.realtime",
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "D"],
|
||||
label: "shortcut.navigation.documentation",
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "S"],
|
||||
label: "shortcut.navigation.settings",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
section: "shortcut.miscellaneous.title",
|
||||
shortcuts: [
|
||||
{
|
||||
keys: [getPlatformSpecialKey(), "M"],
|
||||
label: "shortcut.miscellaneous.invite",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export const spotlight = [
|
||||
{
|
||||
section: "app.spotlight",
|
||||
shortcuts: [
|
||||
{
|
||||
keys: ["?"],
|
||||
label: "shortcut.general.help_menu",
|
||||
action: "modals.support.toggle",
|
||||
icon: "life-buoy",
|
||||
},
|
||||
{
|
||||
keys: [getPlatformSpecialKey(), "K"],
|
||||
label: "shortcut.general.show_all",
|
||||
action: "flyouts.keybinds.toggle",
|
||||
icon: "zap",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
section: "shortcut.navigation.title",
|
||||
shortcuts: [
|
||||
{
|
||||
keys: [getPlatformSpecialKey(), "←"],
|
||||
label: "shortcut.navigation.back",
|
||||
action: "navigation.jump.back",
|
||||
icon: "arrow-right",
|
||||
},
|
||||
{
|
||||
keys: [getPlatformSpecialKey(), "→"],
|
||||
label: "shortcut.navigation.forward",
|
||||
action: "navigation.jump.forward",
|
||||
icon: "arrow-right",
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "R"],
|
||||
label: "shortcut.navigation.rest",
|
||||
action: "navigation.jump.rest",
|
||||
icon: "arrow-right",
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "Q"],
|
||||
label: "shortcut.navigation.graphql",
|
||||
action: "navigation.jump.graphql",
|
||||
icon: "arrow-right",
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "W"],
|
||||
label: "shortcut.navigation.realtime",
|
||||
action: "navigation.jump.realtime",
|
||||
icon: "arrow-right",
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "D"],
|
||||
label: "shortcut.navigation.documentation",
|
||||
action: "navigation.jump.documentation",
|
||||
icon: "arrow-right",
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "S"],
|
||||
label: "shortcut.navigation.settings",
|
||||
action: "navigation.jump.settings",
|
||||
icon: "arrow-right",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
section: "shortcut.miscellaneous.title",
|
||||
shortcuts: [
|
||||
{
|
||||
keys: [getPlatformSpecialKey(), "M"],
|
||||
label: "shortcut.miscellaneous.invite",
|
||||
action: "modals.share.toggle",
|
||||
icon: "gift",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export const fuse = [
|
||||
{
|
||||
keys: ["?"],
|
||||
label: "shortcut.general.help_menu",
|
||||
action: "modals.support.toggle",
|
||||
icon: "life-buoy",
|
||||
tags: [
|
||||
"help",
|
||||
"support",
|
||||
"menu",
|
||||
"discord",
|
||||
"twitter",
|
||||
"documentation",
|
||||
"troubleshooting",
|
||||
"chat",
|
||||
"community",
|
||||
"feedback",
|
||||
"report",
|
||||
"bug",
|
||||
"issue",
|
||||
"ticket",
|
||||
],
|
||||
},
|
||||
{
|
||||
keys: [getPlatformSpecialKey(), "K"],
|
||||
label: "shortcut.general.show_all",
|
||||
action: "flyouts.keybinds.toggle",
|
||||
icon: "zap",
|
||||
tags: ["keyboard", "shortcuts"],
|
||||
},
|
||||
{
|
||||
keys: [getPlatformSpecialKey(), "←"],
|
||||
label: "shortcut.navigation.back",
|
||||
action: "navigation.jump.back",
|
||||
icon: "arrow-right",
|
||||
tags: ["back", "jump", "page", "navigation", "go"],
|
||||
},
|
||||
{
|
||||
keys: [getPlatformSpecialKey(), "→"],
|
||||
label: "shortcut.navigation.forward",
|
||||
action: "navigation.jump.forward",
|
||||
icon: "arrow-right",
|
||||
tags: ["forward", "jump", "next", "forward", "page", "navigation", "go"],
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "R"],
|
||||
label: "shortcut.navigation.rest",
|
||||
action: "navigation.jump.rest",
|
||||
icon: "arrow-right",
|
||||
tags: ["rest", "jump", "page", "navigation", "go"],
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "Q"],
|
||||
label: "shortcut.navigation.graphql",
|
||||
action: "navigation.jump.graphql",
|
||||
icon: "arrow-right",
|
||||
tags: ["graphql", "jump", "page", "navigation", "go"],
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "W"],
|
||||
label: "shortcut.navigation.realtime",
|
||||
action: "navigation.jump.realtime",
|
||||
icon: "arrow-right",
|
||||
tags: [
|
||||
"realtime",
|
||||
"jump",
|
||||
"page",
|
||||
"navigation",
|
||||
"websocket",
|
||||
"socket",
|
||||
"mqtt",
|
||||
"sse",
|
||||
"go",
|
||||
],
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "D"],
|
||||
label: "shortcut.navigation.documentation",
|
||||
action: "navigation.jump.documentation",
|
||||
icon: "arrow-right",
|
||||
tags: ["documentation", "jump", "page", "navigation", "go"],
|
||||
},
|
||||
{
|
||||
keys: [getPlatformAlternateKey(), "S"],
|
||||
label: "shortcut.navigation.settings",
|
||||
action: "navigation.jump.settings",
|
||||
icon: "arrow-right",
|
||||
tags: ["settings", "jump", "page", "navigation", "account", "theme", "go"],
|
||||
},
|
||||
{
|
||||
keys: [getPlatformSpecialKey(), "M"],
|
||||
label: "shortcut.miscellaneous.invite",
|
||||
action: "modals.share.toggle",
|
||||
icon: "gift",
|
||||
tags: ["invite", "share", "app", "friends", "people", "social"],
|
||||
},
|
||||
]
|
||||
79
packages/hoppscotch-app/helpers/strategies/AxiosStrategy.js
Normal file
79
packages/hoppscotch-app/helpers/strategies/AxiosStrategy.js
Normal file
@@ -0,0 +1,79 @@
|
||||
import axios from "axios"
|
||||
import { decodeB64StringToArrayBuffer } from "../utils/b64"
|
||||
import { settingsStore } from "~/newstore/settings"
|
||||
|
||||
let cancelSource = axios.CancelToken.source()
|
||||
|
||||
export const cancelRunningAxiosRequest = () => {
|
||||
cancelSource.cancel()
|
||||
|
||||
// Create a new cancel token
|
||||
cancelSource = axios.CancelToken.source()
|
||||
}
|
||||
|
||||
const axiosWithProxy = async (req) => {
|
||||
try {
|
||||
const { data } = await axios.post(
|
||||
settingsStore.value.PROXY_URL || "https://proxy.hoppscotch.io",
|
||||
{
|
||||
...req,
|
||||
wantsBinary: true,
|
||||
},
|
||||
{
|
||||
cancelToken: cancelSource.token,
|
||||
}
|
||||
)
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.data.message || "Proxy Error")
|
||||
}
|
||||
|
||||
if (data.isBinary) {
|
||||
data.data = decodeB64StringToArrayBuffer(data.data)
|
||||
}
|
||||
|
||||
return data
|
||||
} catch (e) {
|
||||
// Check if the throw is due to a cancellation
|
||||
if (axios.isCancel(e)) {
|
||||
// eslint-disable-next-line no-throw-literal
|
||||
throw "cancellation"
|
||||
} else {
|
||||
console.error(e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const axiosWithoutProxy = async (req, _store) => {
|
||||
try {
|
||||
const res = await axios({
|
||||
...req,
|
||||
cancelToken: (cancelSource && cancelSource.token) || "",
|
||||
responseType: "arraybuffer",
|
||||
})
|
||||
|
||||
return res
|
||||
} catch (e) {
|
||||
if (axios.isCancel(e)) {
|
||||
// eslint-disable-next-line no-throw-literal
|
||||
throw "cancellation"
|
||||
} else {
|
||||
console.error(e)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const axiosStrategy = (req) => {
|
||||
if (settingsStore.value.PROXY_ENABLED) {
|
||||
return axiosWithProxy(req)
|
||||
}
|
||||
return axiosWithoutProxy(req)
|
||||
}
|
||||
|
||||
export const testables = {
|
||||
cancelSource,
|
||||
}
|
||||
|
||||
export default axiosStrategy
|
||||
@@ -0,0 +1,90 @@
|
||||
import { decodeB64StringToArrayBuffer } from "../utils/b64"
|
||||
import { settingsStore } from "~/newstore/settings"
|
||||
|
||||
export const hasExtensionInstalled = () =>
|
||||
typeof window.__POSTWOMAN_EXTENSION_HOOK__ !== "undefined"
|
||||
|
||||
export const hasChromeExtensionInstalled = () =>
|
||||
hasExtensionInstalled() &&
|
||||
/Chrome/i.test(navigator.userAgent) &&
|
||||
/Google/i.test(navigator.vendor)
|
||||
|
||||
export const hasFirefoxExtensionInstalled = () =>
|
||||
hasExtensionInstalled() && /Firefox/i.test(navigator.userAgent)
|
||||
|
||||
export const cancelRunningExtensionRequest = () => {
|
||||
if (
|
||||
hasExtensionInstalled() &&
|
||||
window.__POSTWOMAN_EXTENSION_HOOK__.cancelRunningRequest
|
||||
) {
|
||||
window.__POSTWOMAN_EXTENSION_HOOK__.cancelRunningRequest()
|
||||
}
|
||||
}
|
||||
|
||||
const extensionWithProxy = async (req) => {
|
||||
const backupTimeDataStart = new Date().getTime()
|
||||
|
||||
const res = await window.__POSTWOMAN_EXTENSION_HOOK__.sendRequest({
|
||||
method: "post",
|
||||
url: settingsStore.value.PROXY_URL || "https://proxy.hoppscotch.io/",
|
||||
data: {
|
||||
...req,
|
||||
wantsBinary: true,
|
||||
},
|
||||
})
|
||||
|
||||
const backupTimeDataEnd = new Date().getTime()
|
||||
|
||||
const parsedData = JSON.parse(res.data)
|
||||
|
||||
if (!parsedData.success) {
|
||||
throw new Error(parsedData.data.message || "Proxy Error")
|
||||
}
|
||||
|
||||
if (parsedData.isBinary) {
|
||||
parsedData.data = decodeB64StringToArrayBuffer(parsedData.data)
|
||||
}
|
||||
|
||||
if (!(res && res.config && res.config.timeData)) {
|
||||
res.config = {
|
||||
timeData: {
|
||||
startTime: backupTimeDataStart,
|
||||
endTime: backupTimeDataEnd,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
parsedData.config = res.config
|
||||
|
||||
return parsedData
|
||||
}
|
||||
|
||||
const extensionWithoutProxy = async (req) => {
|
||||
const backupTimeDataStart = new Date().getTime()
|
||||
|
||||
const res = await window.__POSTWOMAN_EXTENSION_HOOK__.sendRequest({
|
||||
...req,
|
||||
wantsBinary: true,
|
||||
})
|
||||
|
||||
const backupTimeDataEnd = new Date().getTime()
|
||||
|
||||
if (!(res && res.config && res.config.timeData)) {
|
||||
res.config = {
|
||||
timeData: {
|
||||
startTime: backupTimeDataStart,
|
||||
endTime: backupTimeDataEnd,
|
||||
},
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
const extensionStrategy = (req) => {
|
||||
if (settingsStore.value.PROXY_ENABLED) {
|
||||
return extensionWithProxy(req)
|
||||
}
|
||||
return extensionWithoutProxy(req)
|
||||
}
|
||||
|
||||
export default extensionStrategy
|
||||
@@ -0,0 +1,59 @@
|
||||
import axios from "axios"
|
||||
import axiosStrategy from "../AxiosStrategy"
|
||||
|
||||
jest.mock("axios")
|
||||
jest.mock("~/newstore/settings", () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
settingsStore: {
|
||||
value: {
|
||||
PROXY_ENABLED: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
axios.CancelToken.source.mockReturnValue({ token: "test" })
|
||||
axios.mockResolvedValue({})
|
||||
|
||||
describe("axiosStrategy", () => {
|
||||
describe("No-Proxy Requests", () => {
|
||||
test("sends request to the actual sender if proxy disabled", async () => {
|
||||
await axiosStrategy({ url: "test" })
|
||||
|
||||
expect(axios).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
url: "test",
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
test("asks axios to return data as arraybuffer", async () => {
|
||||
await axiosStrategy({ url: "test" })
|
||||
|
||||
expect(axios).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
responseType: "arraybuffer",
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
test("resolves successful requests", async () => {
|
||||
await expect(axiosStrategy({})).resolves.toBeDefined()
|
||||
})
|
||||
|
||||
test("rejects cancel errors with text 'cancellation'", () => {
|
||||
axios.isCancel.mockReturnValueOnce(true)
|
||||
axios.mockRejectedValue("err")
|
||||
|
||||
expect(axiosStrategy({})).rejects.toBe("cancellation")
|
||||
})
|
||||
|
||||
test("rejects non-cancellation errors as-is", () => {
|
||||
axios.isCancel.mockReturnValueOnce(false)
|
||||
axios.mockRejectedValue("err")
|
||||
|
||||
expect(axiosStrategy({})).rejects.toBe("err")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,156 @@
|
||||
import axios from "axios"
|
||||
import axiosStrategy, {
|
||||
testables,
|
||||
cancelRunningAxiosRequest,
|
||||
} from "../AxiosStrategy"
|
||||
|
||||
jest.mock("../../utils/b64", () => ({
|
||||
__esModule: true,
|
||||
decodeB64StringToArrayBuffer: jest.fn((data) => `${data}-converted`),
|
||||
}))
|
||||
jest.mock("~/newstore/settings", () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
settingsStore: {
|
||||
value: {
|
||||
PROXY_ENABLED: true,
|
||||
PROXY_URL: "test",
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
describe("cancelRunningAxiosRequest", () => {
|
||||
test("cancels axios request and does that only 1 time", () => {
|
||||
const cancelFunc = jest.spyOn(testables.cancelSource, "cancel")
|
||||
|
||||
cancelRunningAxiosRequest()
|
||||
expect(cancelFunc).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("axiosStrategy", () => {
|
||||
describe("Proxy Requests", () => {
|
||||
test("sends POST request to proxy if proxy is enabled", async () => {
|
||||
let passedURL
|
||||
|
||||
jest.spyOn(axios, "post").mockImplementation((url) => {
|
||||
passedURL = url
|
||||
return Promise.resolve({ data: { success: true, isBinary: false } })
|
||||
})
|
||||
|
||||
await axiosStrategy({})
|
||||
|
||||
expect(passedURL).toEqual("test")
|
||||
})
|
||||
|
||||
test("passes request fields to axios properly", async () => {
|
||||
const reqFields = {
|
||||
testA: "testA",
|
||||
testB: "testB",
|
||||
testC: "testC",
|
||||
}
|
||||
|
||||
let passedFields
|
||||
|
||||
jest.spyOn(axios, "post").mockImplementation((_url, req) => {
|
||||
passedFields = req
|
||||
return Promise.resolve({ data: { success: true, isBinary: false } })
|
||||
})
|
||||
|
||||
await axiosStrategy(reqFields)
|
||||
|
||||
expect(passedFields).toMatchObject(reqFields)
|
||||
})
|
||||
|
||||
test("passes wantsBinary field", async () => {
|
||||
let passedFields
|
||||
|
||||
jest.spyOn(axios, "post").mockImplementation((_url, req) => {
|
||||
passedFields = req
|
||||
return Promise.resolve({ data: { success: true, isBinary: false } })
|
||||
})
|
||||
|
||||
await axiosStrategy({})
|
||||
|
||||
expect(passedFields).toHaveProperty("wantsBinary")
|
||||
})
|
||||
|
||||
test("checks for proxy response success field and throws error message for non-success", async () => {
|
||||
jest.spyOn(axios, "post").mockResolvedValue({
|
||||
data: {
|
||||
success: false,
|
||||
data: {
|
||||
message: "test message",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
await expect(axiosStrategy({})).rejects.toThrow("test message")
|
||||
})
|
||||
|
||||
test("checks for proxy response success field and throws error 'Proxy Error' for non-success", async () => {
|
||||
jest.spyOn(axios, "post").mockResolvedValue({
|
||||
data: {
|
||||
success: false,
|
||||
data: {},
|
||||
},
|
||||
})
|
||||
|
||||
await expect(axiosStrategy({})).rejects.toThrow("Proxy Error")
|
||||
})
|
||||
|
||||
test("checks for proxy response success and doesn't throw for success", async () => {
|
||||
jest.spyOn(axios, "post").mockResolvedValue({
|
||||
data: {
|
||||
success: true,
|
||||
data: {},
|
||||
},
|
||||
})
|
||||
|
||||
await expect(axiosStrategy({})).resolves.toBeDefined()
|
||||
})
|
||||
|
||||
test("checks isBinary response field and resolve with the converted value if so", async () => {
|
||||
jest.spyOn(axios, "post").mockResolvedValue({
|
||||
data: {
|
||||
success: true,
|
||||
isBinary: true,
|
||||
data: "testdata",
|
||||
},
|
||||
})
|
||||
|
||||
await expect(axiosStrategy({})).resolves.toMatchObject({
|
||||
data: "testdata-converted",
|
||||
})
|
||||
})
|
||||
|
||||
test("checks isBinary response field and resolve with the actual value if not so", async () => {
|
||||
jest.spyOn(axios, "post").mockResolvedValue({
|
||||
data: {
|
||||
success: true,
|
||||
isBinary: false,
|
||||
data: "testdata",
|
||||
},
|
||||
})
|
||||
|
||||
await expect(axiosStrategy({})).resolves.toMatchObject({
|
||||
data: "testdata",
|
||||
})
|
||||
})
|
||||
|
||||
test("cancel errors are thrown with the string 'cancellation'", async () => {
|
||||
jest.spyOn(axios, "post").mockRejectedValue("errr")
|
||||
jest.spyOn(axios, "isCancel").mockReturnValueOnce(true)
|
||||
|
||||
await expect(axiosStrategy({})).rejects.toBe("cancellation")
|
||||
})
|
||||
|
||||
test("non-cancellation errors are thrown", async () => {
|
||||
jest.spyOn(axios, "post").mockRejectedValue("errr")
|
||||
jest.spyOn(axios, "isCancel").mockReturnValueOnce(false)
|
||||
|
||||
await expect(axiosStrategy({})).rejects.toBe("errr")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,218 @@
|
||||
import extensionStrategy, {
|
||||
hasExtensionInstalled,
|
||||
hasChromeExtensionInstalled,
|
||||
hasFirefoxExtensionInstalled,
|
||||
cancelRunningExtensionRequest,
|
||||
} from "../ExtensionStrategy"
|
||||
|
||||
jest.mock("../../utils/b64", () => ({
|
||||
__esModule: true,
|
||||
decodeB64StringToArrayBuffer: jest.fn((data) => `${data}-converted`),
|
||||
}))
|
||||
|
||||
jest.mock("~/newstore/settings", () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
settingsStore: {
|
||||
value: {
|
||||
EXTENSIONS_ENABLED: true,
|
||||
PROXY_ENABLED: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
describe("hasExtensionInstalled", () => {
|
||||
test("returns true if extension is present and hooked", () => {
|
||||
global.__POSTWOMAN_EXTENSION_HOOK__ = {}
|
||||
|
||||
expect(hasExtensionInstalled()).toEqual(true)
|
||||
})
|
||||
|
||||
test("returns false if extension not present or not hooked", () => {
|
||||
global.__POSTWOMAN_EXTENSION_HOOK__ = undefined
|
||||
|
||||
expect(hasExtensionInstalled()).toEqual(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("hasChromeExtensionInstalled", () => {
|
||||
test("returns true if extension is hooked and browser is chrome", () => {
|
||||
global.__POSTWOMAN_EXTENSION_HOOK__ = {}
|
||||
jest.spyOn(navigator, "userAgent", "get").mockReturnValue("Chrome")
|
||||
jest.spyOn(navigator, "vendor", "get").mockReturnValue("Google")
|
||||
|
||||
expect(hasChromeExtensionInstalled()).toEqual(true)
|
||||
})
|
||||
|
||||
test("returns false if extension is hooked and browser is not chrome", () => {
|
||||
global.__POSTWOMAN_EXTENSION_HOOK__ = {}
|
||||
jest.spyOn(navigator, "userAgent", "get").mockReturnValue("Firefox")
|
||||
jest.spyOn(navigator, "vendor", "get").mockReturnValue("Google")
|
||||
|
||||
expect(hasChromeExtensionInstalled()).toEqual(false)
|
||||
})
|
||||
|
||||
test("returns false if extension not installed and browser is chrome", () => {
|
||||
global.__POSTWOMAN_EXTENSION_HOOK__ = undefined
|
||||
jest.spyOn(navigator, "userAgent", "get").mockReturnValue("Chrome")
|
||||
jest.spyOn(navigator, "vendor", "get").mockReturnValue("Google")
|
||||
|
||||
expect(hasChromeExtensionInstalled()).toEqual(false)
|
||||
})
|
||||
|
||||
test("returns false if extension not installed and browser is not chrome", () => {
|
||||
global.__POSTWOMAN_EXTENSION_HOOK__ = undefined
|
||||
jest.spyOn(navigator, "userAgent", "get").mockReturnValue("Firefox")
|
||||
jest.spyOn(navigator, "vendor", "get").mockReturnValue("Google")
|
||||
|
||||
expect(hasChromeExtensionInstalled()).toEqual(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("hasFirefoxExtensionInstalled", () => {
|
||||
test("returns true if extension is hooked and browser is firefox", () => {
|
||||
global.__POSTWOMAN_EXTENSION_HOOK__ = {}
|
||||
jest.spyOn(navigator, "userAgent", "get").mockReturnValue("Firefox")
|
||||
|
||||
expect(hasFirefoxExtensionInstalled()).toEqual(true)
|
||||
})
|
||||
|
||||
test("returns false if extension is hooked and browser is not firefox", () => {
|
||||
global.__POSTWOMAN_EXTENSION_HOOK__ = {}
|
||||
jest.spyOn(navigator, "userAgent", "get").mockReturnValue("Chrome")
|
||||
|
||||
expect(hasFirefoxExtensionInstalled()).toEqual(false)
|
||||
})
|
||||
|
||||
test("returns false if extension not installed and browser is firefox", () => {
|
||||
global.__POSTWOMAN_EXTENSION_HOOK__ = undefined
|
||||
jest.spyOn(navigator, "userAgent", "get").mockReturnValue("Firefox")
|
||||
|
||||
expect(hasFirefoxExtensionInstalled()).toEqual(false)
|
||||
})
|
||||
|
||||
test("returns false if extension not installed and browser is not firefox", () => {
|
||||
global.__POSTWOMAN_EXTENSION_HOOK__ = undefined
|
||||
jest.spyOn(navigator, "userAgent", "get").mockReturnValue("Chrome")
|
||||
|
||||
expect(hasFirefoxExtensionInstalled()).toEqual(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("cancelRunningExtensionRequest", () => {
|
||||
const cancelFunc = jest.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
cancelFunc.mockClear()
|
||||
})
|
||||
|
||||
test("cancels request if extension installed and function present in hook", () => {
|
||||
global.__POSTWOMAN_EXTENSION_HOOK__ = {
|
||||
cancelRunningRequest: cancelFunc,
|
||||
}
|
||||
|
||||
cancelRunningExtensionRequest()
|
||||
expect(cancelFunc).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
test("does not cancel request if extension not installed", () => {
|
||||
global.__POSTWOMAN_EXTENSION_HOOK__ = undefined
|
||||
|
||||
cancelRunningExtensionRequest()
|
||||
expect(cancelFunc).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("does not cancel request if extension installed but function not present", () => {
|
||||
global.__POSTWOMAN_EXTENSION_HOOK__ = {}
|
||||
|
||||
cancelRunningExtensionRequest()
|
||||
expect(cancelFunc).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("extensionStrategy", () => {
|
||||
const sendReqFunc = jest.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
sendReqFunc.mockClear()
|
||||
})
|
||||
|
||||
describe("Non-Proxy Requests", () => {
|
||||
test("ask extension to send request", async () => {
|
||||
global.__POSTWOMAN_EXTENSION_HOOK__ = {
|
||||
sendRequest: sendReqFunc,
|
||||
}
|
||||
|
||||
sendReqFunc.mockResolvedValue({
|
||||
data: '{"success":true,"data":""}',
|
||||
})
|
||||
|
||||
await extensionStrategy({})
|
||||
|
||||
expect(sendReqFunc).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
test("sends request to the actual sender if proxy disabled", async () => {
|
||||
let passedUrl
|
||||
|
||||
global.__POSTWOMAN_EXTENSION_HOOK__ = {
|
||||
sendRequest: sendReqFunc,
|
||||
}
|
||||
|
||||
sendReqFunc.mockImplementation(({ url }) => {
|
||||
passedUrl = url
|
||||
|
||||
return Promise.resolve({
|
||||
data: '{"success":true,"data":""}',
|
||||
})
|
||||
})
|
||||
|
||||
await extensionStrategy({ url: "test" })
|
||||
|
||||
expect(passedUrl).toEqual("test")
|
||||
})
|
||||
|
||||
test("asks extension to get binary data", async () => {
|
||||
let passedFields
|
||||
|
||||
global.__POSTWOMAN_EXTENSION_HOOK__ = {
|
||||
sendRequest: sendReqFunc,
|
||||
}
|
||||
|
||||
sendReqFunc.mockImplementation((fields) => {
|
||||
passedFields = fields
|
||||
|
||||
return Promise.resolve({
|
||||
data: '{"success":true,"data":""}',
|
||||
})
|
||||
})
|
||||
|
||||
await extensionStrategy({})
|
||||
|
||||
expect(passedFields).toHaveProperty("wantsBinary")
|
||||
})
|
||||
|
||||
test("resolves successful requests", async () => {
|
||||
global.__POSTWOMAN_EXTENSION_HOOK__ = {
|
||||
sendRequest: sendReqFunc,
|
||||
}
|
||||
|
||||
sendReqFunc.mockResolvedValue({
|
||||
data: '{"success":true,"data":""}',
|
||||
})
|
||||
|
||||
await expect(extensionStrategy({})).resolves.toBeDefined()
|
||||
})
|
||||
|
||||
test("rejects errors as-is", async () => {
|
||||
global.__POSTWOMAN_EXTENSION_HOOK__ = {
|
||||
sendRequest: sendReqFunc,
|
||||
}
|
||||
|
||||
sendReqFunc.mockRejectedValue("err")
|
||||
|
||||
await expect(extensionStrategy({})).rejects.toBe("err")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,300 @@
|
||||
import extensionStrategy, {
|
||||
hasExtensionInstalled,
|
||||
hasChromeExtensionInstalled,
|
||||
hasFirefoxExtensionInstalled,
|
||||
cancelRunningExtensionRequest,
|
||||
} from "../ExtensionStrategy"
|
||||
|
||||
jest.mock("../../utils/b64", () => ({
|
||||
__esModule: true,
|
||||
decodeB64StringToArrayBuffer: jest.fn((data) => `${data}-converted`),
|
||||
}))
|
||||
|
||||
jest.mock("~/newstore/settings", () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
settingsStore: {
|
||||
value: {
|
||||
EXTENSIONS_ENABLED: true,
|
||||
PROXY_ENABLED: true,
|
||||
PROXY_URL: "test",
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
describe("hasExtensionInstalled", () => {
|
||||
test("returns true if extension is present and hooked", () => {
|
||||
global.__POSTWOMAN_EXTENSION_HOOK__ = {}
|
||||
|
||||
expect(hasExtensionInstalled()).toEqual(true)
|
||||
})
|
||||
|
||||
test("returns false if extension not present or not hooked", () => {
|
||||
global.__POSTWOMAN_EXTENSION_HOOK__ = undefined
|
||||
|
||||
expect(hasExtensionInstalled()).toEqual(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("hasChromeExtensionInstalled", () => {
|
||||
test("returns true if extension is hooked and browser is chrome", () => {
|
||||
global.__POSTWOMAN_EXTENSION_HOOK__ = {}
|
||||
jest.spyOn(navigator, "userAgent", "get").mockReturnValue("Chrome")
|
||||
jest.spyOn(navigator, "vendor", "get").mockReturnValue("Google")
|
||||
|
||||
expect(hasChromeExtensionInstalled()).toEqual(true)
|
||||
})
|
||||
|
||||
test("returns false if extension is hooked and browser is not chrome", () => {
|
||||
global.__POSTWOMAN_EXTENSION_HOOK__ = {}
|
||||
jest.spyOn(navigator, "userAgent", "get").mockReturnValue("Firefox")
|
||||
jest.spyOn(navigator, "vendor", "get").mockReturnValue("Google")
|
||||
|
||||
expect(hasChromeExtensionInstalled()).toEqual(false)
|
||||
})
|
||||
|
||||
test("returns false if extension not installed and browser is chrome", () => {
|
||||
global.__POSTWOMAN_EXTENSION_HOOK__ = undefined
|
||||
jest.spyOn(navigator, "userAgent", "get").mockReturnValue("Chrome")
|
||||
jest.spyOn(navigator, "vendor", "get").mockReturnValue("Google")
|
||||
|
||||
expect(hasChromeExtensionInstalled()).toEqual(false)
|
||||
})
|
||||
|
||||
test("returns false if extension not installed and browser is not chrome", () => {
|
||||
global.__POSTWOMAN_EXTENSION_HOOK__ = undefined
|
||||
jest.spyOn(navigator, "userAgent", "get").mockReturnValue("Firefox")
|
||||
jest.spyOn(navigator, "vendor", "get").mockReturnValue("Google")
|
||||
|
||||
expect(hasChromeExtensionInstalled()).toEqual(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("hasFirefoxExtensionInstalled", () => {
|
||||
test("returns true if extension is hooked and browser is firefox", () => {
|
||||
global.__POSTWOMAN_EXTENSION_HOOK__ = {}
|
||||
jest.spyOn(navigator, "userAgent", "get").mockReturnValue("Firefox")
|
||||
|
||||
expect(hasFirefoxExtensionInstalled()).toEqual(true)
|
||||
})
|
||||
|
||||
test("returns false if extension is hooked and browser is not firefox", () => {
|
||||
global.__POSTWOMAN_EXTENSION_HOOK__ = {}
|
||||
jest.spyOn(navigator, "userAgent", "get").mockReturnValue("Chrome")
|
||||
|
||||
expect(hasFirefoxExtensionInstalled()).toEqual(false)
|
||||
})
|
||||
|
||||
test("returns false if extension not installed and browser is firefox", () => {
|
||||
global.__POSTWOMAN_EXTENSION_HOOK__ = undefined
|
||||
jest.spyOn(navigator, "userAgent", "get").mockReturnValue("Firefox")
|
||||
|
||||
expect(hasFirefoxExtensionInstalled()).toEqual(false)
|
||||
})
|
||||
|
||||
test("returns false if extension not installed and browser is not firefox", () => {
|
||||
global.__POSTWOMAN_EXTENSION_HOOK__ = undefined
|
||||
jest.spyOn(navigator, "userAgent", "get").mockReturnValue("Chrome")
|
||||
|
||||
expect(hasFirefoxExtensionInstalled()).toEqual(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe("cancelRunningExtensionRequest", () => {
|
||||
const cancelFunc = jest.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
cancelFunc.mockClear()
|
||||
})
|
||||
|
||||
test("cancels request if extension installed and function present in hook", () => {
|
||||
global.__POSTWOMAN_EXTENSION_HOOK__ = {
|
||||
cancelRunningRequest: cancelFunc,
|
||||
}
|
||||
|
||||
cancelRunningExtensionRequest()
|
||||
expect(cancelFunc).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
test("does not cancel request if extension not installed", () => {
|
||||
global.__POSTWOMAN_EXTENSION_HOOK__ = undefined
|
||||
|
||||
cancelRunningExtensionRequest()
|
||||
expect(cancelFunc).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("does not cancel request if extension installed but function not present", () => {
|
||||
global.__POSTWOMAN_EXTENSION_HOOK__ = {}
|
||||
|
||||
cancelRunningExtensionRequest()
|
||||
expect(cancelFunc).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe("extensionStrategy", () => {
|
||||
const sendReqFunc = jest.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
sendReqFunc.mockClear()
|
||||
})
|
||||
|
||||
describe("Proxy Requests", () => {
|
||||
test("asks extension to send request", async () => {
|
||||
global.__POSTWOMAN_EXTENSION_HOOK__ = {
|
||||
sendRequest: sendReqFunc,
|
||||
}
|
||||
|
||||
sendReqFunc.mockResolvedValue({
|
||||
data: '{"success":true,"data":""}',
|
||||
})
|
||||
|
||||
await extensionStrategy({})
|
||||
|
||||
expect(sendReqFunc).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
test("sends POST request to proxy if proxy is enabled", async () => {
|
||||
let passedUrl
|
||||
let passedMethod
|
||||
|
||||
global.__POSTWOMAN_EXTENSION_HOOK__ = {
|
||||
sendRequest: sendReqFunc,
|
||||
}
|
||||
|
||||
sendReqFunc.mockImplementation(({ method, url }) => {
|
||||
passedUrl = url
|
||||
passedMethod = method
|
||||
|
||||
return Promise.resolve({
|
||||
data: '{"success":true,"data":""}',
|
||||
})
|
||||
})
|
||||
|
||||
await extensionStrategy({})
|
||||
|
||||
expect(passedUrl).toEqual("test")
|
||||
expect(passedMethod).toEqual("post")
|
||||
})
|
||||
|
||||
test("passes request fields properly", async () => {
|
||||
const reqFields = {
|
||||
testA: "testA",
|
||||
testB: "testB",
|
||||
testC: "testC",
|
||||
}
|
||||
|
||||
let passedFields
|
||||
|
||||
global.__POSTWOMAN_EXTENSION_HOOK__ = {
|
||||
sendRequest: sendReqFunc,
|
||||
}
|
||||
|
||||
sendReqFunc.mockImplementation(({ data }) => {
|
||||
passedFields = data
|
||||
|
||||
return Promise.resolve({
|
||||
data: '{"success":true,"data":""}',
|
||||
})
|
||||
})
|
||||
|
||||
await extensionStrategy(reqFields)
|
||||
|
||||
expect(passedFields).toMatchObject(reqFields)
|
||||
})
|
||||
|
||||
test("passes wantsBinary field", async () => {
|
||||
let passedFields
|
||||
|
||||
global.__POSTWOMAN_EXTENSION_HOOK__ = {
|
||||
sendRequest: sendReqFunc,
|
||||
}
|
||||
|
||||
sendReqFunc.mockImplementation(({ data }) => {
|
||||
passedFields = data
|
||||
|
||||
return Promise.resolve({
|
||||
data: '{"success":true,"data":""}',
|
||||
})
|
||||
})
|
||||
|
||||
await extensionStrategy({})
|
||||
|
||||
expect(passedFields).toHaveProperty("wantsBinary")
|
||||
})
|
||||
|
||||
test("checks for proxy response success field and throws error message for non-success", async () => {
|
||||
global.__POSTWOMAN_EXTENSION_HOOK__ = {
|
||||
sendRequest: sendReqFunc,
|
||||
}
|
||||
|
||||
sendReqFunc.mockResolvedValue({
|
||||
data: '{"success":false,"data": { "message": "testerr" } }',
|
||||
})
|
||||
|
||||
await expect(extensionStrategy({})).rejects.toThrow("testerr")
|
||||
})
|
||||
|
||||
test("checks for proxy response success field and throws error 'Proxy Error' for non-success", async () => {
|
||||
global.__POSTWOMAN_EXTENSION_HOOK__ = {
|
||||
sendRequest: sendReqFunc,
|
||||
}
|
||||
|
||||
sendReqFunc.mockResolvedValue({
|
||||
data: '{"success":false,"data": {} }',
|
||||
})
|
||||
|
||||
await expect(extensionStrategy({})).rejects.toThrow("Proxy Error")
|
||||
})
|
||||
|
||||
test("checks for proxy response success and doesn't throw for success", async () => {
|
||||
global.__POSTWOMAN_EXTENSION_HOOK__ = {
|
||||
sendRequest: sendReqFunc,
|
||||
}
|
||||
|
||||
sendReqFunc.mockResolvedValue({
|
||||
data: '{"success":true,"data": {} }',
|
||||
})
|
||||
|
||||
await expect(extensionStrategy({})).resolves.toBeDefined()
|
||||
})
|
||||
|
||||
test("checks isBinary response field and resolve with the converted value if so", async () => {
|
||||
global.__POSTWOMAN_EXTENSION_HOOK__ = {
|
||||
sendRequest: sendReqFunc,
|
||||
}
|
||||
|
||||
sendReqFunc.mockResolvedValue({
|
||||
data: '{"success": true, "isBinary": true, "data": "testdata" }',
|
||||
})
|
||||
|
||||
await expect(extensionStrategy({})).resolves.toMatchObject({
|
||||
data: "testdata-converted",
|
||||
})
|
||||
})
|
||||
|
||||
test("checks isBinary response field and resolve with the actual value if not so", async () => {
|
||||
global.__POSTWOMAN_EXTENSION_HOOK__ = {
|
||||
sendRequest: sendReqFunc,
|
||||
}
|
||||
|
||||
sendReqFunc.mockResolvedValue({
|
||||
data: '{"success": true, "isBinary": false, "data": "testdata" }',
|
||||
})
|
||||
|
||||
await expect(extensionStrategy({})).resolves.toMatchObject({
|
||||
data: "testdata",
|
||||
})
|
||||
})
|
||||
|
||||
test("failed request errors are thrown as-is", async () => {
|
||||
global.__POSTWOMAN_EXTENSION_HOOK__ = {
|
||||
sendRequest: sendReqFunc,
|
||||
}
|
||||
|
||||
sendReqFunc.mockRejectedValue("err")
|
||||
|
||||
await expect(extensionStrategy({})).rejects.toBe("err")
|
||||
})
|
||||
})
|
||||
})
|
||||
4
packages/hoppscotch-app/helpers/support.js
Normal file
4
packages/hoppscotch-app/helpers/support.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export const showChat = () => {
|
||||
$crisp.push(["do", "chat:show"])
|
||||
$crisp.push(["do", "chat:open"])
|
||||
}
|
||||
118
packages/hoppscotch-app/helpers/syntax/gqlQueryLangMode.js
Normal file
118
packages/hoppscotch-app/helpers/syntax/gqlQueryLangMode.js
Normal file
@@ -0,0 +1,118 @@
|
||||
export function defineGQLLanguageMode(ace) {
|
||||
// Highlighting
|
||||
ace.define(
|
||||
"ace/mode/gql-query-highlight",
|
||||
["require", "exports", "ace/lib/oop", "ace/mode/text_highlight_rules"],
|
||||
(aceRequire, exports) => {
|
||||
const oop = aceRequire("ace/lib/oop")
|
||||
|
||||
const TextHighlightRules = aceRequire(
|
||||
"ace/mode/text_highlight_rules"
|
||||
).TextHighlightRules
|
||||
|
||||
const GQLQueryTextHighlightRules = function () {
|
||||
const keywords =
|
||||
"type|interface|union|enum|schema|input|implements|extends|scalar|fragment|query|mutation|subscription"
|
||||
|
||||
const dataTypes = "Int|Float|String|ID|Boolean"
|
||||
|
||||
const literalValues = "true|false|null"
|
||||
|
||||
const escapeRe = /\\(?:u[\da-fA-f]{4}|.)/
|
||||
|
||||
const keywordMapper = this.createKeywordMapper(
|
||||
{
|
||||
keyword: keywords,
|
||||
"storage.type": dataTypes,
|
||||
"constant.language": literalValues,
|
||||
},
|
||||
"identifier"
|
||||
)
|
||||
|
||||
this.$rules = {
|
||||
start: [
|
||||
{
|
||||
token: "comment",
|
||||
regex: "#.*$",
|
||||
},
|
||||
{
|
||||
token: "paren.lparen",
|
||||
regex: /[[({]/,
|
||||
next: "start",
|
||||
},
|
||||
{
|
||||
token: "paren.rparen",
|
||||
regex: /[\])}]/,
|
||||
},
|
||||
{
|
||||
token: keywordMapper,
|
||||
regex: "[a-zA-Z_][a-zA-Z0-9_$]*\\b",
|
||||
},
|
||||
{
|
||||
token: "string", // character
|
||||
regex: `'(?:${escapeRe}|.)?'`,
|
||||
},
|
||||
{
|
||||
token: "string.start",
|
||||
regex: '"',
|
||||
stateName: "qqstring",
|
||||
next: [
|
||||
{ token: "string", regex: /\\\s*$/, next: "qqstring" },
|
||||
{ token: "constant.language.escape", regex: escapeRe },
|
||||
{ token: "string.end", regex: '"|$', next: "start" },
|
||||
{ defaultToken: "string" },
|
||||
],
|
||||
},
|
||||
{
|
||||
token: "string.start",
|
||||
regex: "'",
|
||||
stateName: "singleQuoteString",
|
||||
next: [
|
||||
{ token: "string", regex: /\\\s*$/, next: "singleQuoteString" },
|
||||
{ token: "constant.language.escape", regex: escapeRe },
|
||||
{ token: "string.end", regex: "'|$", next: "start" },
|
||||
{ defaultToken: "string" },
|
||||
],
|
||||
},
|
||||
{
|
||||
token: "constant.numeric",
|
||||
regex: /\d+\.?\d*[eE]?[+-]?\d*/,
|
||||
},
|
||||
{
|
||||
token: "variable",
|
||||
regex: /\$[_A-Za-z][_0-9A-Za-z]*/,
|
||||
},
|
||||
],
|
||||
}
|
||||
this.normalizeRules()
|
||||
}
|
||||
|
||||
oop.inherits(GQLQueryTextHighlightRules, TextHighlightRules)
|
||||
|
||||
exports.GQLQueryTextHighlightRules = GQLQueryTextHighlightRules
|
||||
}
|
||||
)
|
||||
|
||||
// Language Mode Definition
|
||||
ace.define(
|
||||
"ace/mode/gql-query",
|
||||
["require", "exports", "ace/mode/text", "ace/mode/gql-query-highlight"],
|
||||
(aceRequire, exports) => {
|
||||
const oop = aceRequire("ace/lib/oop")
|
||||
const TextMode = aceRequire("ace/mode/text").Mode
|
||||
const GQLQueryTextHighlightRules = aceRequire(
|
||||
"ace/mode/gql-query-highlight"
|
||||
).GQLQueryTextHighlightRules
|
||||
const FoldMode = aceRequire("ace/mode/folding/cstyle").FoldMode
|
||||
|
||||
const Mode = function () {
|
||||
this.HighlightRules = GQLQueryTextHighlightRules
|
||||
this.foldingRules = new FoldMode()
|
||||
}
|
||||
|
||||
oop.inherits(Mode, TextMode)
|
||||
|
||||
exports.Mode = Mode
|
||||
}
|
||||
)
|
||||
}
|
||||
84
packages/hoppscotch-app/helpers/teams/BackendUserInfo.ts
Normal file
84
packages/hoppscotch-app/helpers/teams/BackendUserInfo.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
import { BehaviorSubject } from "rxjs"
|
||||
import gql from "graphql-tag"
|
||||
import { authIdToken$ } from "../fb/auth"
|
||||
import { apolloClient } from "../apollo"
|
||||
|
||||
/*
|
||||
* This file deals with interfacing data provided by the
|
||||
* Hoppscotch Backend server
|
||||
*/
|
||||
|
||||
/**
|
||||
* Defines the information provided about a user
|
||||
*/
|
||||
export interface UserInfo {
|
||||
/**
|
||||
* UID of the user
|
||||
*/
|
||||
uid: string
|
||||
/**
|
||||
* Displayable name of the user (or null if none available)
|
||||
*/
|
||||
displayName: string | null
|
||||
/**
|
||||
* Email of the user (or null if none available)
|
||||
*/
|
||||
email: string | null
|
||||
/**
|
||||
* URL to the profile photo of the user (or null if none available)
|
||||
*/
|
||||
photoURL: string | null
|
||||
/**
|
||||
* Whether the user has access to Early Access features
|
||||
*/
|
||||
eaInvited: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* An observable subject onto the currently logged in user info (is null if not logged in)
|
||||
*/
|
||||
export const currentUserInfo$ = new BehaviorSubject<UserInfo | null>(null)
|
||||
|
||||
/**
|
||||
* Initializes the currenUserInfo$ view and sets up its update mechanism
|
||||
*/
|
||||
export function initUserInfo() {
|
||||
authIdToken$.subscribe((token) => {
|
||||
if (token) {
|
||||
updateUserInfo()
|
||||
} else {
|
||||
currentUserInfo$.next(null)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the actual user info fetching
|
||||
*/
|
||||
async function updateUserInfo() {
|
||||
try {
|
||||
const { data } = await apolloClient.query({
|
||||
query: gql`
|
||||
query GetUserInfo {
|
||||
me {
|
||||
uid
|
||||
displayName
|
||||
email
|
||||
photoURL
|
||||
eaInvited
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
|
||||
currentUserInfo$.next({
|
||||
uid: data.me.uid,
|
||||
displayName: data.me.displayName,
|
||||
email: data.me.email,
|
||||
photoURL: data.me.photoURL,
|
||||
eaInvited: data.me.eaInvited,
|
||||
})
|
||||
} catch (e) {
|
||||
currentUserInfo$.next(null)
|
||||
}
|
||||
}
|
||||
11
packages/hoppscotch-app/helpers/teams/TeamCollection.ts
Normal file
11
packages/hoppscotch-app/helpers/teams/TeamCollection.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { TeamRequest } from "./TeamRequest"
|
||||
|
||||
/**
|
||||
* Defines how a Team Collection is represented in the TeamCollectionAdapter
|
||||
*/
|
||||
export interface TeamCollection {
|
||||
id: string
|
||||
title: string
|
||||
children: TeamCollection[] | null
|
||||
requests: TeamRequest[] | null
|
||||
}
|
||||
563
packages/hoppscotch-app/helpers/teams/TeamCollectionAdapter.ts
Normal file
563
packages/hoppscotch-app/helpers/teams/TeamCollectionAdapter.ts
Normal file
@@ -0,0 +1,563 @@
|
||||
import { BehaviorSubject } from "rxjs"
|
||||
import { gql } from "graphql-tag"
|
||||
import pull from "lodash/pull"
|
||||
import remove from "lodash/remove"
|
||||
import { translateToNewRequest } from "../types/HoppRESTRequest"
|
||||
import { TeamCollection } from "./TeamCollection"
|
||||
import { TeamRequest } from "./TeamRequest"
|
||||
import {
|
||||
rootCollectionsOfTeam,
|
||||
getCollectionChildren,
|
||||
getCollectionRequests,
|
||||
} from "./utils"
|
||||
import { apolloClient } from "~/helpers/apollo"
|
||||
|
||||
/*
|
||||
* NOTE: These functions deal with REFERENCES to objects and mutates them, for a simpler implementation.
|
||||
* Be careful when you play with these.
|
||||
*
|
||||
* I am not a fan of mutating references but this is so much simpler compared to mutating clones
|
||||
* - Andrew
|
||||
*/
|
||||
|
||||
/**
|
||||
* Finds the parent of a collection and returns the REFERENCE (or null)
|
||||
*
|
||||
* @param {TeamCollection[]} tree - The tree to look in
|
||||
* @param {string} collID - ID of the collection to find the parent of
|
||||
* @param {TeamCollection} currentParent - (used for recursion, do not set) The parent in the current iteration (undefined if root)
|
||||
*
|
||||
* @returns REFERENCE to the collecton or null if not found or the collection is in root
|
||||
*/
|
||||
function findParentOfColl(
|
||||
tree: TeamCollection[],
|
||||
collID: string,
|
||||
currentParent?: TeamCollection
|
||||
): TeamCollection | null {
|
||||
for (const coll of tree) {
|
||||
// If the root is parent, return null
|
||||
if (coll.id === collID) return currentParent || null
|
||||
|
||||
// Else run it in children
|
||||
if (coll.children) {
|
||||
const result = findParentOfColl(coll.children, collID, coll)
|
||||
if (result) return result
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and returns a REFERENCE collection in the given tree (or null)
|
||||
*
|
||||
* @param {TeamCollection[]} tree - The tree to look in
|
||||
* @param {string} targetID - The ID of the collection to look for
|
||||
*
|
||||
* @returns REFERENCE to the collection or null if not found
|
||||
*/
|
||||
function findCollInTree(
|
||||
tree: TeamCollection[],
|
||||
targetID: string
|
||||
): TeamCollection | null {
|
||||
for (const coll of tree) {
|
||||
// If the direct child matched, then return that
|
||||
if (coll.id === targetID) return coll
|
||||
|
||||
// Else run it in the children
|
||||
if (coll.children) {
|
||||
const result = findCollInTree(coll.children, targetID)
|
||||
if (result) return result
|
||||
}
|
||||
}
|
||||
|
||||
// If nothing matched, return null
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and returns a REFERENCE to the collection containing a given request ID in tree (or null)
|
||||
*
|
||||
* @param {TeamCollection[]} tree - The tree to look in
|
||||
* @param {string} reqID - The ID of the request to look for
|
||||
*
|
||||
* @returns REFERENCE to the collection or null if request not found
|
||||
*/
|
||||
function findCollWithReqIDInTree(
|
||||
tree: TeamCollection[],
|
||||
reqID: string
|
||||
): TeamCollection | null {
|
||||
for (const coll of tree) {
|
||||
// Check in root collections (if expanded)
|
||||
if (coll.requests) {
|
||||
if (coll.requests.find((req) => req.id === reqID)) return coll
|
||||
}
|
||||
|
||||
// Check in children of collections
|
||||
if (coll.children) {
|
||||
const result = findCollWithReqIDInTree(coll.children, reqID)
|
||||
if (result) return result
|
||||
}
|
||||
}
|
||||
|
||||
// No matches
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and returns a REFERENCE to the request with the given ID (or null)
|
||||
*
|
||||
* @param {TeamCollection[]} tree - The tree to look in
|
||||
* @param {string} reqID - The ID of the request to look for
|
||||
*
|
||||
* @returns REFERENCE to the request or null if request not found
|
||||
*/
|
||||
function findReqInTree(
|
||||
tree: TeamCollection[],
|
||||
reqID: string
|
||||
): TeamRequest | null {
|
||||
for (const coll of tree) {
|
||||
// Check in root collections (if expanded)
|
||||
if (coll.requests) {
|
||||
const match = coll.requests.find((req) => req.id === reqID)
|
||||
if (match) return match
|
||||
}
|
||||
|
||||
// Check in children of collections
|
||||
if (coll.children) {
|
||||
const match = findReqInTree(coll.children, reqID)
|
||||
if (match) return match
|
||||
}
|
||||
}
|
||||
|
||||
// No matches
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a collection in the tree with the specified data
|
||||
*
|
||||
* @param {TeamCollection[]} tree - The tree to update in (THIS WILL BE MUTATED!)
|
||||
* @param {Partial<TeamCollection> & Pick<TeamCollection, "id">} updateColl - An object defining all the fields that should be updated (ID is required to find the target collection)
|
||||
*/
|
||||
function updateCollInTree(
|
||||
tree: TeamCollection[],
|
||||
updateColl: Partial<TeamCollection> & Pick<TeamCollection, "id">
|
||||
) {
|
||||
const el = findCollInTree(tree, updateColl.id)
|
||||
|
||||
// If no match, stop the operation
|
||||
if (!el) return
|
||||
|
||||
// Update all the specified keys
|
||||
Object.assign(el, updateColl)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a collection in the tree
|
||||
*
|
||||
* @param {TeamCollection[]} tree - The tree to delete in (THIS WILL BE MUTATED!)
|
||||
* @param {string} targetID - ID of the collection to delete
|
||||
*/
|
||||
function deleteCollInTree(tree: TeamCollection[], targetID: string) {
|
||||
// Get the parent owning the collection
|
||||
const parent = findParentOfColl(tree, targetID)
|
||||
|
||||
// If we found a parent, update it
|
||||
if (parent && parent.children) {
|
||||
parent.children = parent.children.filter((coll) => coll.id !== targetID)
|
||||
}
|
||||
|
||||
// If there is no parent, it could mean:
|
||||
// 1. The collection with that ID does not exist
|
||||
// 2. The collection is in root (therefore, no parent)
|
||||
|
||||
// Let's look for element, if not exist, then stop
|
||||
const el = findCollInTree(tree, targetID)
|
||||
if (!el) return
|
||||
|
||||
// Collection exists, so this should be in root, hence removing element
|
||||
pull(tree, el)
|
||||
}
|
||||
|
||||
/**
|
||||
* TeamCollectionAdapter provides a reactive collections list for a specific team
|
||||
*/
|
||||
export default class TeamCollectionAdapter {
|
||||
/**
|
||||
* The reactive list of collections
|
||||
*
|
||||
* A new value is emitted when there is a change
|
||||
* (Use views instead)
|
||||
*/
|
||||
collections$: BehaviorSubject<TeamCollection[]>
|
||||
|
||||
// Fields for subscriptions, used for destroying once not needed
|
||||
private teamCollectionAdded$: ZenObservable.Subscription | null
|
||||
private teamCollectionUpdated$: ZenObservable.Subscription | null
|
||||
private teamCollectionRemoved$: ZenObservable.Subscription | null
|
||||
private teamRequestAdded$: ZenObservable.Subscription | null
|
||||
private teamRequestUpdated$: ZenObservable.Subscription | null
|
||||
private teamRequestDeleted$: ZenObservable.Subscription | null
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*
|
||||
* @param {string | null} teamID - ID of the team to listen to, or null if none decided and the adapter should stand by
|
||||
*/
|
||||
constructor(private teamID: string | null) {
|
||||
this.collections$ = new BehaviorSubject<TeamCollection[]>([])
|
||||
this.teamCollectionAdded$ = null
|
||||
this.teamCollectionUpdated$ = null
|
||||
this.teamCollectionRemoved$ = null
|
||||
this.teamRequestAdded$ = null
|
||||
this.teamRequestDeleted$ = null
|
||||
this.teamRequestUpdated$ = null
|
||||
|
||||
if (this.teamID) this.initialize()
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the team the adapter is looking at
|
||||
*
|
||||
* @param {string | null} newTeamID - ID of the team to listen to, or null if none decided and the adapter should stand by
|
||||
*/
|
||||
changeTeamID(newTeamID: string | null) {
|
||||
this.collections$.next([])
|
||||
|
||||
this.teamID = newTeamID
|
||||
|
||||
if (this.teamID) this.initialize()
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribes from the subscriptions
|
||||
* NOTE: Once this is called, no new updates to the tree will be detected
|
||||
*/
|
||||
unsubscribeSubscriptions() {
|
||||
this.teamCollectionAdded$?.unsubscribe()
|
||||
this.teamCollectionUpdated$?.unsubscribe()
|
||||
this.teamCollectionRemoved$?.unsubscribe()
|
||||
this.teamRequestAdded$?.unsubscribe()
|
||||
this.teamRequestDeleted$?.unsubscribe()
|
||||
this.teamRequestUpdated$?.unsubscribe()
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the adapter
|
||||
*/
|
||||
private async initialize() {
|
||||
await this.loadRootCollections()
|
||||
this.registerSubscriptions()
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the root collections
|
||||
*/
|
||||
private async loadRootCollections(): Promise<void> {
|
||||
const colls = await rootCollectionsOfTeam(apolloClient, this.teamID)
|
||||
this.collections$.next(colls)
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs addition of a collection to the tree
|
||||
*
|
||||
* @param {TeamCollection} collection - The collection to add to the tree
|
||||
* @param {string | null} parentCollectionID - The parent of the new collection, pass null if this collection is in root
|
||||
*/
|
||||
private addCollection(
|
||||
collection: TeamCollection,
|
||||
parentCollectionID: string | null
|
||||
) {
|
||||
const tree = this.collections$.value
|
||||
|
||||
if (!parentCollectionID) {
|
||||
tree.push(collection)
|
||||
} else {
|
||||
const parentCollection = findCollInTree(tree, parentCollectionID)
|
||||
|
||||
if (!parentCollection) return
|
||||
|
||||
if (parentCollection.children != null) {
|
||||
parentCollection.children.push(collection)
|
||||
} else {
|
||||
parentCollection.children = [collection]
|
||||
}
|
||||
}
|
||||
|
||||
this.collections$.next(tree)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing collection in tree
|
||||
*
|
||||
* @param {Partial<TeamCollection> & Pick<TeamCollection, "id">} collectionUpdate - Object defining the fields that need to be updated (ID is required to find the target)
|
||||
*/
|
||||
private updateCollection(
|
||||
collectionUpdate: Partial<TeamCollection> & Pick<TeamCollection, "id">
|
||||
) {
|
||||
const tree = this.collections$.value
|
||||
|
||||
updateCollInTree(tree, collectionUpdate)
|
||||
|
||||
this.collections$.next(tree)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a collection from the tree
|
||||
*
|
||||
* @param {string} collectionID - ID of the collection to remove
|
||||
*/
|
||||
private removeCollection(collectionID: string) {
|
||||
const tree = this.collections$.value
|
||||
|
||||
deleteCollInTree(tree, collectionID)
|
||||
|
||||
this.collections$.next(tree)
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a request to the tree
|
||||
*
|
||||
* @param {TeamRequest} request - The request to add to the tree
|
||||
*/
|
||||
private addRequest(request: TeamRequest) {
|
||||
const tree = this.collections$.value
|
||||
|
||||
// Check if we have the collection (if not, then not loaded?)
|
||||
const coll = findCollInTree(tree, request.collectionID)
|
||||
if (!coll) return // Ignore add request
|
||||
|
||||
// Collection is not expanded
|
||||
if (!coll.requests) return
|
||||
|
||||
// Collection is expanded hence append request
|
||||
coll.requests.push(request)
|
||||
|
||||
this.collections$.next(tree)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a request from the tree
|
||||
*
|
||||
* @param {string} requestID - ID of the request to remove
|
||||
*/
|
||||
private removeRequest(requestID: string) {
|
||||
const tree = this.collections$.value
|
||||
|
||||
// Find request in tree, don't attempt if no collection or no requests (expansion?)
|
||||
const coll = findCollWithReqIDInTree(tree, requestID)
|
||||
if (!coll || !coll.requests) return
|
||||
|
||||
// Remove the collection
|
||||
remove(coll.requests, (req) => req.id === requestID)
|
||||
|
||||
// Publish new tree
|
||||
this.collections$.next(tree)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the request in tree
|
||||
*
|
||||
* @param {Partial<TeamRequest> & Pick<TeamRequest, 'id'>} requestUpdate - Object defining all the fields to update in request (ID of the request is required)
|
||||
*/
|
||||
private updateRequest(
|
||||
requestUpdate: Partial<TeamRequest> & Pick<TeamRequest, "id">
|
||||
) {
|
||||
const tree = this.collections$.value
|
||||
|
||||
// Find request, if not present, don't update
|
||||
const req = findReqInTree(tree, requestUpdate.id)
|
||||
if (!req) return
|
||||
|
||||
Object.assign(req, requestUpdate)
|
||||
|
||||
this.collections$.next(tree)
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers the subscriptions to listen to team collection updates
|
||||
*/
|
||||
registerSubscriptions() {
|
||||
this.teamCollectionAdded$ = apolloClient
|
||||
.subscribe({
|
||||
query: gql`
|
||||
subscription TeamCollectionAdded($teamID: String!) {
|
||||
teamCollectionAdded(teamID: $teamID) {
|
||||
id
|
||||
title
|
||||
parent {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
teamID: this.teamID,
|
||||
},
|
||||
})
|
||||
.subscribe(({ data }) => {
|
||||
this.addCollection(
|
||||
{
|
||||
id: data.teamCollectionAdded.id,
|
||||
children: null,
|
||||
requests: null,
|
||||
title: data.teamCollectionAdded.title,
|
||||
},
|
||||
data.teamCollectionAdded.parent?.id
|
||||
)
|
||||
})
|
||||
|
||||
this.teamCollectionUpdated$ = apolloClient
|
||||
.subscribe({
|
||||
query: gql`
|
||||
subscription TeamCollectionUpdated($teamID: String!) {
|
||||
teamCollectionUpdated(teamID: $teamID) {
|
||||
id
|
||||
title
|
||||
parent {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
teamID: this.teamID,
|
||||
},
|
||||
})
|
||||
.subscribe(({ data }) => {
|
||||
this.updateCollection({
|
||||
id: data.teamCollectionUpdated.id,
|
||||
title: data.teamCollectionUpdated.title,
|
||||
})
|
||||
})
|
||||
|
||||
this.teamCollectionRemoved$ = apolloClient
|
||||
.subscribe({
|
||||
query: gql`
|
||||
subscription TeamCollectionRemoved($teamID: String!) {
|
||||
teamCollectionRemoved(teamID: $teamID)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
teamID: this.teamID,
|
||||
},
|
||||
})
|
||||
.subscribe(({ data }) => {
|
||||
this.removeCollection(data.teamCollectionRemoved)
|
||||
})
|
||||
|
||||
this.teamRequestAdded$ = apolloClient
|
||||
.subscribe({
|
||||
query: gql`
|
||||
subscription TeamRequestAdded($teamID: String!) {
|
||||
teamRequestAdded(teamID: $teamID) {
|
||||
id
|
||||
collectionID
|
||||
request
|
||||
title
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
teamID: this.teamID,
|
||||
},
|
||||
})
|
||||
.subscribe(({ data }) => {
|
||||
this.addRequest({
|
||||
id: data.teamRequestAdded.id,
|
||||
collectionID: data.teamRequestAdded.collectionID,
|
||||
request: translateToNewRequest(
|
||||
JSON.parse(data.teamRequestAdded.request)
|
||||
),
|
||||
title: data.teamRequestAdded.title,
|
||||
})
|
||||
})
|
||||
|
||||
this.teamRequestUpdated$ = apolloClient
|
||||
.subscribe({
|
||||
query: gql`
|
||||
subscription TeamRequestUpdated($teamID: String!) {
|
||||
teamRequestUpdated(teamID: $teamID) {
|
||||
id
|
||||
collectionID
|
||||
request
|
||||
title
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
teamID: this.teamID,
|
||||
},
|
||||
})
|
||||
.subscribe(({ data }) => {
|
||||
this.updateRequest({
|
||||
id: data.teamRequestUpdated.id,
|
||||
collectionID: data.teamRequestUpdated.collectionID,
|
||||
request: JSON.parse(data.teamRequestUpdated.request),
|
||||
title: data.teamRequestUpdated.title,
|
||||
})
|
||||
})
|
||||
|
||||
this.teamRequestDeleted$ = apolloClient
|
||||
.subscribe({
|
||||
query: gql`
|
||||
subscription TeamRequestDeleted($teamID: String!) {
|
||||
teamRequestDeleted(teamID: $teamID)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
teamID: this.teamID,
|
||||
},
|
||||
})
|
||||
.subscribe(({ data }) => {
|
||||
this.removeRequest(data.teamRequestDeleted)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands a collection on the tree
|
||||
*
|
||||
* When a collection is loaded initially in the adapter, children and requests are not loaded (they will be set to null)
|
||||
* Upon expansion those two fields will be populated
|
||||
*
|
||||
* @param {string} collectionID - The ID of the collection to expand
|
||||
*/
|
||||
async expandCollection(collectionID: string): Promise<void> {
|
||||
// TODO: While expanding one collection, block (or queue) the expansion of the other, to avoid race conditions
|
||||
const tree = this.collections$.value
|
||||
|
||||
const collection = findCollInTree(tree, collectionID)
|
||||
|
||||
if (!collection) return
|
||||
|
||||
if (collection.children != null) return
|
||||
|
||||
const collections: TeamCollection[] = (
|
||||
await getCollectionChildren(apolloClient, collectionID)
|
||||
).map<TeamCollection>((el) => {
|
||||
return {
|
||||
id: el.id,
|
||||
title: el.title,
|
||||
children: null,
|
||||
requests: null,
|
||||
}
|
||||
})
|
||||
|
||||
const requests: TeamRequest[] = (
|
||||
await getCollectionRequests(apolloClient, collectionID)
|
||||
).map<TeamRequest>((el) => {
|
||||
return {
|
||||
id: el.id,
|
||||
collectionID,
|
||||
title: el.title,
|
||||
request: translateToNewRequest(JSON.parse(el.request)),
|
||||
}
|
||||
})
|
||||
|
||||
collection.children = collections
|
||||
collection.requests = requests
|
||||
|
||||
this.collections$.next(tree)
|
||||
}
|
||||
}
|
||||
160
packages/hoppscotch-app/helpers/teams/TeamMemberAdapter.ts
Normal file
160
packages/hoppscotch-app/helpers/teams/TeamMemberAdapter.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
import { BehaviorSubject } from "rxjs"
|
||||
import gql from "graphql-tag"
|
||||
import cloneDeep from "lodash/cloneDeep"
|
||||
import * as Apollo from "@apollo/client/core"
|
||||
import { apolloClient } from "~/helpers/apollo"
|
||||
|
||||
interface TeamsTeamMember {
|
||||
membershipID: string
|
||||
user: {
|
||||
uid: string
|
||||
email: string
|
||||
}
|
||||
role: "OWNER" | "EDITOR" | "VIEWER"
|
||||
}
|
||||
|
||||
export default class TeamMemberAdapter {
|
||||
members$: BehaviorSubject<TeamsTeamMember[]>
|
||||
|
||||
private teamMemberAdded$: ZenObservable.Subscription | null
|
||||
private teamMemberRemoved$: ZenObservable.Subscription | null
|
||||
private teamMemberUpdated$: ZenObservable.Subscription | null
|
||||
|
||||
constructor(private teamID: string | null) {
|
||||
this.members$ = new BehaviorSubject<TeamsTeamMember[]>([])
|
||||
|
||||
this.teamMemberAdded$ = null
|
||||
this.teamMemberUpdated$ = null
|
||||
this.teamMemberRemoved$ = null
|
||||
|
||||
if (this.teamID) this.initialize()
|
||||
}
|
||||
|
||||
changeTeamID(newTeamID: string | null) {
|
||||
this.members$.next([])
|
||||
|
||||
this.teamID = newTeamID
|
||||
|
||||
if (this.teamID) this.initialize()
|
||||
}
|
||||
|
||||
unsubscribeSubscriptions() {
|
||||
this.teamMemberAdded$?.unsubscribe()
|
||||
this.teamMemberRemoved$?.unsubscribe()
|
||||
this.teamMemberUpdated$?.unsubscribe()
|
||||
}
|
||||
|
||||
private async initialize() {
|
||||
await this.loadTeamMembers()
|
||||
this.registerSubscriptions()
|
||||
}
|
||||
|
||||
private async loadTeamMembers(): Promise<void> {
|
||||
const result: TeamsTeamMember[] = []
|
||||
|
||||
let cursor: string | null = null
|
||||
while (true) {
|
||||
const response: Apollo.ApolloQueryResult<any> = await apolloClient.query({
|
||||
query: gql`
|
||||
query GetTeamMembers($teamID: String!, $cursor: String) {
|
||||
team(teamID: $teamID) {
|
||||
members(cursor: $cursor) {
|
||||
membershipID
|
||||
user {
|
||||
uid
|
||||
email
|
||||
}
|
||||
role
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
teamID: this.teamID,
|
||||
cursor,
|
||||
},
|
||||
})
|
||||
|
||||
result.push(...response.data.team.members)
|
||||
|
||||
if ((response.data.team.members as any[]).length === 0) break
|
||||
else {
|
||||
cursor =
|
||||
response.data.team.members[response.data.team.members.length - 1]
|
||||
.membershipID
|
||||
}
|
||||
}
|
||||
|
||||
this.members$.next(result)
|
||||
}
|
||||
|
||||
private registerSubscriptions() {
|
||||
this.teamMemberAdded$ = apolloClient
|
||||
.subscribe({
|
||||
query: gql`
|
||||
subscription TeamMemberAdded($teamID: String!) {
|
||||
teamMemberAdded(teamID: $teamID) {
|
||||
user {
|
||||
uid
|
||||
email
|
||||
}
|
||||
role
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
teamID: this.teamID,
|
||||
},
|
||||
})
|
||||
.subscribe(({ data }) => {
|
||||
this.members$.next([...this.members$.value, data.teamMemberAdded])
|
||||
})
|
||||
|
||||
this.teamMemberRemoved$ = apolloClient
|
||||
.subscribe({
|
||||
query: gql`
|
||||
subscription TeamMemberRemoved($teamID: String!) {
|
||||
teamMemberRemoved(teamID: $teamID)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
teamID: this.teamID,
|
||||
},
|
||||
})
|
||||
.subscribe(({ data }) => {
|
||||
this.members$.next(
|
||||
this.members$.value.filter(
|
||||
(el) => el.user.uid !== data.teamMemberRemoved
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
this.teamMemberUpdated$ = apolloClient
|
||||
.subscribe({
|
||||
query: gql`
|
||||
subscription TeamMemberUpdated($teamID: String!) {
|
||||
teamMemberUpdated(teamID: $teamID) {
|
||||
user {
|
||||
uid
|
||||
email
|
||||
}
|
||||
role
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
teamID: this.teamID,
|
||||
},
|
||||
})
|
||||
.subscribe(({ data }) => {
|
||||
const list = cloneDeep(this.members$.value)
|
||||
const obj = list.find(
|
||||
(el) => el.user.uid === data.teamMemberUpdated.user.uid
|
||||
)
|
||||
|
||||
if (!obj) return
|
||||
|
||||
Object.assign(obj, data.teamMemberUpdated)
|
||||
})
|
||||
}
|
||||
}
|
||||
11
packages/hoppscotch-app/helpers/teams/TeamRequest.ts
Normal file
11
packages/hoppscotch-app/helpers/teams/TeamRequest.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { HoppRESTRequest } from "../types/HoppRESTRequest"
|
||||
|
||||
/**
|
||||
* Defines how a Teams request is represented in TeamCollectionAdapter
|
||||
*/
|
||||
export interface TeamRequest {
|
||||
id: string
|
||||
collectionID: string
|
||||
title: string
|
||||
request: HoppRESTRequest
|
||||
}
|
||||
576
packages/hoppscotch-app/helpers/teams/utils.js
Normal file
576
packages/hoppscotch-app/helpers/teams/utils.js
Normal file
@@ -0,0 +1,576 @@
|
||||
import gql from "graphql-tag"
|
||||
import { BehaviorSubject } from "rxjs"
|
||||
|
||||
/**
|
||||
* Returns an observable list of team members in the given Team
|
||||
*
|
||||
* @param {ApolloClient<any>} apollo - Instance of ApolloClient
|
||||
* @param {string} teamID - ID of the team to observe
|
||||
*
|
||||
* @returns {{user: {uid: string, email: string}, role: 'OWNER' | 'EDITOR' | 'VIEWER'}}
|
||||
*/
|
||||
export async function getLiveTeamMembersList(apollo, teamID) {
|
||||
const subject = new BehaviorSubject([])
|
||||
|
||||
const { data } = await apollo.query({
|
||||
query: gql`
|
||||
query GetTeamMembers($teamID: String!) {
|
||||
team(teamID: $teamID) {
|
||||
members {
|
||||
user {
|
||||
uid
|
||||
email
|
||||
}
|
||||
role
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
teamID,
|
||||
},
|
||||
})
|
||||
|
||||
subject.next(data.team.members)
|
||||
|
||||
const addedSub = apollo
|
||||
.subscribe({
|
||||
query: gql`
|
||||
subscription TeamMemberAdded($teamID: String!) {
|
||||
teamMemberAdded(teamID: $teamID) {
|
||||
user {
|
||||
uid
|
||||
email
|
||||
}
|
||||
role
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
teamID,
|
||||
},
|
||||
})
|
||||
.subscribe(({ data }) => {
|
||||
subject.next([...subject.value, data.teamMemberAdded])
|
||||
})
|
||||
|
||||
const updateSub = apollo
|
||||
.subscribe({
|
||||
query: gql`
|
||||
subscription TeamMemberUpdated($teamID: String!) {
|
||||
teamMemberUpdated(teamID: $teamID) {
|
||||
user {
|
||||
uid
|
||||
email
|
||||
}
|
||||
role
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
teamID,
|
||||
},
|
||||
})
|
||||
.subscribe(({ data }) => {
|
||||
const val = subject.value.find(
|
||||
(member) => member.user.uid === data.teamMemberUpdated.user.uid
|
||||
)
|
||||
|
||||
if (!val) return
|
||||
|
||||
Object.assign(val, data.teamMemberUpdated)
|
||||
})
|
||||
|
||||
const removeSub = apollo
|
||||
.subscribe({
|
||||
query: gql`
|
||||
subscription TeamMemberRemoved($teamID: String!) {
|
||||
teamMemberRemoved(teamID: $teamID)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
teamID,
|
||||
},
|
||||
})
|
||||
.subscribe(({ data }) => {
|
||||
subject.next(
|
||||
subject.value.filter(
|
||||
(member) => member.user.uid !== data.teamMemberAdded.user.uid
|
||||
)
|
||||
)
|
||||
})
|
||||
|
||||
const mainSub = subject.subscribe({
|
||||
complete() {
|
||||
addedSub.unsubscribe()
|
||||
updateSub.unsubscribe()
|
||||
removeSub.unsubscribe()
|
||||
|
||||
mainSub.unsubscribe()
|
||||
},
|
||||
})
|
||||
|
||||
return subject
|
||||
}
|
||||
|
||||
export function createTeam(apollo, name) {
|
||||
return apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation ($name: String!) {
|
||||
createTeam(name: $name) {
|
||||
name
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
name,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function addTeamMemberByEmail(apollo, userRole, userEmail, teamID) {
|
||||
return apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation addTeamMemberByEmail(
|
||||
$userRole: TeamMemberRole!
|
||||
$userEmail: String!
|
||||
$teamID: String!
|
||||
) {
|
||||
addTeamMemberByEmail(
|
||||
userRole: $userRole
|
||||
userEmail: $userEmail
|
||||
teamID: $teamID
|
||||
) {
|
||||
role
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
userRole,
|
||||
userEmail,
|
||||
teamID,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function updateTeamMemberRole(apollo, userID, newRole, teamID) {
|
||||
return apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation updateTeamMemberRole(
|
||||
$newRole: TeamMemberRole!
|
||||
$userUid: String!
|
||||
$teamID: String!
|
||||
) {
|
||||
updateTeamMemberRole(
|
||||
newRole: $newRole
|
||||
userUid: $userUid
|
||||
teamID: $teamID
|
||||
) {
|
||||
role
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
newRole,
|
||||
userUid: userID,
|
||||
teamID,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function renameTeam(apollo, name, teamID) {
|
||||
return apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation renameTeam($newName: String!, $teamID: String!) {
|
||||
renameTeam(newName: $newName, teamID: $teamID) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
newName: name,
|
||||
teamID,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function removeTeamMember(apollo, userID, teamID) {
|
||||
return apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation removeTeamMember($userUid: String!, $teamID: String!) {
|
||||
removeTeamMember(userUid: $userUid, teamID: $teamID)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
userUid: userID,
|
||||
teamID,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function deleteTeam(apollo, teamID) {
|
||||
let response
|
||||
while (true) {
|
||||
response = await apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation ($teamID: String!) {
|
||||
deleteTeam(teamID: $teamID)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
teamID,
|
||||
},
|
||||
})
|
||||
if (response !== undefined) break
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
export function exitTeam(apollo, teamID) {
|
||||
return apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation ($teamID: String!) {
|
||||
leaveTeam(teamID: $teamID)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
teamID,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function rootCollectionsOfTeam(apollo, teamID) {
|
||||
const collections = []
|
||||
let cursor = ""
|
||||
while (true) {
|
||||
const response = await apollo.query({
|
||||
query: gql`
|
||||
query rootCollectionsOfTeam($teamID: String!, $cursor: String!) {
|
||||
rootCollectionsOfTeam(teamID: $teamID, cursor: $cursor) {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
teamID,
|
||||
cursor,
|
||||
},
|
||||
fetchPolicy: "no-cache",
|
||||
})
|
||||
if (response.data.rootCollectionsOfTeam.length === 0) break
|
||||
response.data.rootCollectionsOfTeam.forEach((collection) => {
|
||||
collections.push(collection)
|
||||
})
|
||||
cursor = collections[collections.length - 1].id
|
||||
}
|
||||
return collections
|
||||
}
|
||||
|
||||
export async function getCollectionChildren(apollo, collectionID) {
|
||||
const children = []
|
||||
const response = await apollo.query({
|
||||
query: gql`
|
||||
query getCollectionChildren($collectionID: String!) {
|
||||
collection(collectionID: $collectionID) {
|
||||
children {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
collectionID,
|
||||
},
|
||||
fetchPolicy: "no-cache",
|
||||
})
|
||||
response.data.collection.children.forEach((child) => {
|
||||
children.push(child)
|
||||
})
|
||||
return children
|
||||
}
|
||||
|
||||
export async function getCollectionRequests(apollo, collectionID) {
|
||||
const requests = []
|
||||
let cursor = ""
|
||||
while (true) {
|
||||
const response = await apollo.query({
|
||||
query: gql`
|
||||
query getCollectionRequests($collectionID: String!, $cursor: String) {
|
||||
requestsInCollection(collectionID: $collectionID, cursor: $cursor) {
|
||||
id
|
||||
title
|
||||
request
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
collectionID,
|
||||
cursor,
|
||||
},
|
||||
fetchPolicy: "no-cache",
|
||||
})
|
||||
|
||||
response.data.requestsInCollection.forEach((request) => {
|
||||
requests.push(request)
|
||||
})
|
||||
|
||||
if (response.data.requestsInCollection.length < 10) {
|
||||
break
|
||||
}
|
||||
cursor = requests[requests.length - 1].id
|
||||
}
|
||||
return requests
|
||||
}
|
||||
|
||||
export async function renameCollection(apollo, title, id) {
|
||||
let response
|
||||
while (true) {
|
||||
response = await apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation ($newTitle: String!, $collectionID: String!) {
|
||||
renameCollection(newTitle: $newTitle, collectionID: $collectionID) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
newTitle: title,
|
||||
collectionID: id,
|
||||
},
|
||||
})
|
||||
if (response !== undefined) break
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
export async function updateRequest(apollo, request, requestName, requestID) {
|
||||
let response
|
||||
while (true) {
|
||||
response = await apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation ($data: UpdateTeamRequestInput!, $requestID: String!) {
|
||||
updateRequest(data: $data, requestID: $requestID) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
data: {
|
||||
request: JSON.stringify(request),
|
||||
title: requestName,
|
||||
},
|
||||
requestID,
|
||||
},
|
||||
})
|
||||
if (response !== undefined) break
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
export async function addChildCollection(apollo, title, id) {
|
||||
let response
|
||||
while (true) {
|
||||
response = await apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation ($childTitle: String!, $collectionID: String!) {
|
||||
createChildCollection(
|
||||
childTitle: $childTitle
|
||||
collectionID: $collectionID
|
||||
) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
childTitle: title,
|
||||
collectionID: id,
|
||||
},
|
||||
})
|
||||
if (response !== undefined) break
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
export async function deleteCollection(apollo, id) {
|
||||
let response
|
||||
while (true) {
|
||||
response = await apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation ($collectionID: String!) {
|
||||
deleteCollection(collectionID: $collectionID)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
collectionID: id,
|
||||
},
|
||||
})
|
||||
if (response !== undefined) break
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
export async function deleteRequest(apollo, requestID) {
|
||||
let response
|
||||
while (true) {
|
||||
response = await apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation ($requestID: String!) {
|
||||
deleteRequest(requestID: $requestID)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
requestID,
|
||||
},
|
||||
})
|
||||
if (response !== undefined) break
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
export async function createNewRootCollection(apollo, title, id) {
|
||||
let response
|
||||
while (true) {
|
||||
response = await apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation ($title: String!, $teamID: String!) {
|
||||
createRootCollection(title: $title, teamID: $teamID) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
title,
|
||||
teamID: id,
|
||||
},
|
||||
})
|
||||
if (response !== undefined) break
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
export async function saveRequestAsTeams(
|
||||
apollo,
|
||||
request,
|
||||
title,
|
||||
teamID,
|
||||
collectionID
|
||||
) {
|
||||
const x = await apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation ($data: CreateTeamRequestInput!, $collectionID: String!) {
|
||||
createRequestInCollection(data: $data, collectionID: $collectionID) {
|
||||
id
|
||||
collection {
|
||||
id
|
||||
team {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
collectionID,
|
||||
data: {
|
||||
teamID,
|
||||
title,
|
||||
request,
|
||||
},
|
||||
},
|
||||
})
|
||||
return x.data?.createRequestInCollection
|
||||
}
|
||||
|
||||
export async function overwriteRequestTeams(apollo, request, title, requestID) {
|
||||
await apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation updateRequest(
|
||||
$data: UpdateTeamRequestInput!
|
||||
$requestID: String!
|
||||
) {
|
||||
updateRequest(data: $data, requestID: $requestID) {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
requestID,
|
||||
data: {
|
||||
request,
|
||||
title,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export async function importFromMyCollections(apollo, collectionID, teamID) {
|
||||
const response = await apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation importFromMyCollections(
|
||||
$fbCollectionPath: String!
|
||||
$teamID: String!
|
||||
) {
|
||||
importCollectionFromUserFirestore(
|
||||
fbCollectionPath: $fbCollectionPath
|
||||
teamID: $teamID
|
||||
) {
|
||||
id
|
||||
title
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
fbCollectionPath: collectionID,
|
||||
teamID,
|
||||
},
|
||||
})
|
||||
return response.data != null
|
||||
}
|
||||
|
||||
export async function importFromJSON(apollo, collections, teamID) {
|
||||
const response = await apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation importFromJSON($jsonString: String!, $teamID: String!) {
|
||||
importCollectionsFromJSON(jsonString: $jsonString, teamID: $teamID)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
jsonString: JSON.stringify(collections),
|
||||
teamID,
|
||||
},
|
||||
})
|
||||
return response.data != null
|
||||
}
|
||||
|
||||
export async function replaceWithJSON(apollo, collections, teamID) {
|
||||
const response = await apollo.mutate({
|
||||
mutation: gql`
|
||||
mutation replaceWithJSON($jsonString: String!, $teamID: String!) {
|
||||
replaceCollectionsWithJSON(jsonString: $jsonString, teamID: $teamID)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
jsonString: JSON.stringify(collections),
|
||||
teamID,
|
||||
},
|
||||
})
|
||||
return response.data != null
|
||||
}
|
||||
|
||||
export async function exportAsJSON(apollo, teamID) {
|
||||
const response = await apollo.query({
|
||||
query: gql`
|
||||
query exportAsJSON($teamID: String!) {
|
||||
exportCollectionsToJSON(teamID: $teamID)
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
teamID,
|
||||
},
|
||||
})
|
||||
return response.data.exportCollectionsToJSON
|
||||
}
|
||||
15
packages/hoppscotch-app/helpers/templating.ts
Normal file
15
packages/hoppscotch-app/helpers/templating.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { Environment } from "~/newstore/environments"
|
||||
|
||||
export default function parseTemplateString(
|
||||
str: string,
|
||||
variables: Environment["variables"]
|
||||
) {
|
||||
if (!variables || !str) {
|
||||
return str
|
||||
}
|
||||
const searchTerm = /<<([^>]*)>>/g // "<<myVariable>>"
|
||||
return decodeURI(encodeURI(str)).replace(
|
||||
searchTerm,
|
||||
(_, p1) => variables.find((x) => x.key === p1)?.value || ""
|
||||
)
|
||||
}
|
||||
117
packages/hoppscotch-app/helpers/tern.js
Normal file
117
packages/hoppscotch-app/helpers/tern.js
Normal file
@@ -0,0 +1,117 @@
|
||||
import tern from "tern"
|
||||
import { registerTernLinter } from "./ternlint"
|
||||
import ECMA_DEF from "~/helpers/terndoc/ecma.json"
|
||||
import PW_PRE_DEF from "~/helpers/terndoc/pw-pre.json"
|
||||
import PW_TEST_DEF from "~/helpers/terndoc/pw-test.json"
|
||||
import PW_EXTRAS_DEF from "~/helpers/terndoc/pw-extras.json"
|
||||
|
||||
const server = new tern.Server({
|
||||
defs: [ECMA_DEF, PW_EXTRAS_DEF],
|
||||
plugins: {
|
||||
lint: {
|
||||
rules: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
registerTernLinter()
|
||||
|
||||
function performLinting(code) {
|
||||
return new Promise((resolve, reject) => {
|
||||
server.request(
|
||||
{
|
||||
query: {
|
||||
type: "lint",
|
||||
file: "doc",
|
||||
lineCharPositions: true,
|
||||
},
|
||||
files: [
|
||||
{
|
||||
type: "full",
|
||||
name: "doc",
|
||||
text: code,
|
||||
},
|
||||
],
|
||||
},
|
||||
(err, res) => {
|
||||
if (!err) resolve(res.messages)
|
||||
else reject(err)
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export function performPreRequestLinting(code) {
|
||||
server.deleteDefs("pw-test")
|
||||
server.deleteDefs("pw-pre")
|
||||
server.addDefs(PW_PRE_DEF)
|
||||
return performLinting(code)
|
||||
}
|
||||
|
||||
export function performTestLinting(code) {
|
||||
server.deleteDefs("pw-test")
|
||||
server.deleteDefs("pw-pre")
|
||||
server.addDefs(PW_TEST_DEF)
|
||||
return performLinting(code)
|
||||
}
|
||||
|
||||
function postProcessCompletionResult(res) {
|
||||
if (res.completions) {
|
||||
const index = res.completions.findIndex((el) => el.name === "pw")
|
||||
|
||||
if (index !== -1) {
|
||||
const result = res.completions[index]
|
||||
|
||||
res.completions.splice(index, 1)
|
||||
res.completions.splice(0, 0, result)
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
function performCompletion(code, row, col) {
|
||||
return new Promise((resolve, reject) => {
|
||||
server.request(
|
||||
{
|
||||
query: {
|
||||
type: "completions",
|
||||
file: "doc",
|
||||
end: {
|
||||
line: row,
|
||||
ch: col,
|
||||
},
|
||||
guess: false,
|
||||
types: true,
|
||||
includeKeywords: true,
|
||||
inLiteral: false,
|
||||
},
|
||||
files: [
|
||||
{
|
||||
type: "full",
|
||||
name: "doc",
|
||||
text: code,
|
||||
},
|
||||
],
|
||||
},
|
||||
(err, res) => {
|
||||
if (err) reject(err)
|
||||
else resolve(postProcessCompletionResult(res))
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
export function getPreRequestScriptCompletions(code, row, col) {
|
||||
server.deleteDefs("pw-test")
|
||||
server.deleteDefs("pw-pre")
|
||||
server.addDefs(PW_PRE_DEF)
|
||||
return performCompletion(code, row, col)
|
||||
}
|
||||
|
||||
export function getTestScriptCompletions(code, row, col) {
|
||||
server.deleteDefs("pw-test")
|
||||
server.deleteDefs("pw-pre")
|
||||
server.addDefs(PW_TEST_DEF)
|
||||
return performCompletion(code, row, col)
|
||||
}
|
||||
1284
packages/hoppscotch-app/helpers/terndoc/ecma.json
Normal file
1284
packages/hoppscotch-app/helpers/terndoc/ecma.json
Normal file
File diff suppressed because it is too large
Load Diff
51
packages/hoppscotch-app/helpers/terndoc/pw-extras.json
Normal file
51
packages/hoppscotch-app/helpers/terndoc/pw-extras.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"!name": "pw-extra",
|
||||
"console": {
|
||||
"assert": {
|
||||
"!type": "fn(assertion: bool, text: string)"
|
||||
},
|
||||
"clear": {
|
||||
"!type": "fn()"
|
||||
},
|
||||
"count": {
|
||||
"!type": "fn(label?: string)"
|
||||
},
|
||||
"debug": "console.log",
|
||||
"dir": {
|
||||
"!type": "fn(object: ?)"
|
||||
},
|
||||
"error": {
|
||||
"!type": "fn(...msg: ?)"
|
||||
},
|
||||
"group": {
|
||||
"!type": "fn(label?: string)"
|
||||
},
|
||||
"groupCollapsed": {
|
||||
"!type": "fn(label?: string)"
|
||||
},
|
||||
"groupEnd": {
|
||||
"!type": "fn()"
|
||||
},
|
||||
"info": {
|
||||
"!type": "fn(...msg: ?)"
|
||||
},
|
||||
"log": {
|
||||
"!type": "fn(...msg: ?)"
|
||||
},
|
||||
"table": {
|
||||
"!type": "fn(data: []|?, columns?: [])"
|
||||
},
|
||||
"time": {
|
||||
"!type": "fn(label: string)"
|
||||
},
|
||||
"timeEnd": {
|
||||
"!type": "fn(label: string)"
|
||||
},
|
||||
"trace": {
|
||||
"!type": "fn()"
|
||||
},
|
||||
"warn": {
|
||||
"!type": "fn(...msg: ?)"
|
||||
}
|
||||
}
|
||||
}
|
||||
8
packages/hoppscotch-app/helpers/terndoc/pw-pre.json
Normal file
8
packages/hoppscotch-app/helpers/terndoc/pw-pre.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"!name": "pw-pre",
|
||||
"pw": {
|
||||
"env": {
|
||||
"set": "fn(key: string, value: string)"
|
||||
}
|
||||
}
|
||||
}
|
||||
24
packages/hoppscotch-app/helpers/terndoc/pw-test.json
Normal file
24
packages/hoppscotch-app/helpers/terndoc/pw-test.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"!name": "pw-test",
|
||||
"!define": {
|
||||
"Expectation": {
|
||||
"not": "Expectation",
|
||||
"toBe": "fn(value: ?)",
|
||||
"toBeLevel2xx": "fn()",
|
||||
"toBeLevel3xx": "fn()",
|
||||
"toBeLevel4xx": "fn()",
|
||||
"toBeLevel5xx": "fn()",
|
||||
"toBeType": "fn(type: string)",
|
||||
"toHaveLength": "fn(length: number)"
|
||||
}
|
||||
},
|
||||
"pw": {
|
||||
"expect": "fn(value: ?) -> Expectation",
|
||||
"response": {
|
||||
"status": "number",
|
||||
"headers": "?",
|
||||
"body": "?"
|
||||
},
|
||||
"test": "fn(name: string, func: fn())"
|
||||
}
|
||||
}
|
||||
722
packages/hoppscotch-app/helpers/ternlint.js
Normal file
722
packages/hoppscotch-app/helpers/ternlint.js
Normal file
@@ -0,0 +1,722 @@
|
||||
/* eslint-disable */
|
||||
|
||||
import infer from "tern/lib/infer"
|
||||
import tern from "tern/lib/tern"
|
||||
|
||||
const walk = require("acorn-walk")
|
||||
|
||||
var defaultRules = {
|
||||
UnknownProperty: { severity: "warning" },
|
||||
UnknownIdentifier: { severity: "warning" },
|
||||
NotAFunction: { severity: "error" },
|
||||
InvalidArgument: { severity: "error" },
|
||||
UnusedVariable: { severity: "warning" },
|
||||
UnknownModule: { severity: "error" },
|
||||
MixedReturnTypes: { severity: "warning" },
|
||||
ObjectLiteral: { severity: "error" },
|
||||
TypeMismatch: { severity: "warning" },
|
||||
Array: { severity: "error" },
|
||||
ES6Modules: { severity: "error" },
|
||||
}
|
||||
|
||||
function makeVisitors(server, query, file, messages) {
|
||||
function addMessage(node, msg, severity) {
|
||||
var error = makeError(node, msg, severity)
|
||||
messages.push(error)
|
||||
}
|
||||
|
||||
function makeError(node, msg, severity) {
|
||||
var pos = getPosition(node)
|
||||
var error = {
|
||||
message: msg,
|
||||
from: tern.outputPos(query, file, pos.start),
|
||||
to: tern.outputPos(query, file, pos.end),
|
||||
severity: severity,
|
||||
}
|
||||
if (query.lineNumber) {
|
||||
error.lineNumber = query.lineCharPositions
|
||||
? error.from.line
|
||||
: tern.outputPos({ lineCharPositions: true }, file, pos.start).line
|
||||
}
|
||||
if (!query.groupByFiles) error.file = file.name
|
||||
return error
|
||||
}
|
||||
|
||||
function getNodeName(node) {
|
||||
if (node.callee) {
|
||||
// This is a CallExpression node.
|
||||
// We get the position of the function name.
|
||||
return getNodeName(node.callee)
|
||||
} else if (node.property) {
|
||||
// This is a MemberExpression node.
|
||||
// We get the name of the property.
|
||||
return node.property.name
|
||||
} else {
|
||||
return node.name
|
||||
}
|
||||
}
|
||||
|
||||
function getNodeValue(node) {
|
||||
if (node.callee) {
|
||||
// This is a CallExpression node.
|
||||
// We get the position of the function name.
|
||||
return getNodeValue(node.callee)
|
||||
} else if (node.property) {
|
||||
// This is a MemberExpression node.
|
||||
// We get the value of the property.
|
||||
return node.property.value
|
||||
} else {
|
||||
if (node.type === "Identifier") {
|
||||
var query = { type: "definition", start: node.start, end: node.end }
|
||||
var expr = tern.findQueryExpr(file, query)
|
||||
var type = infer.expressionType(expr)
|
||||
var objExpr = type.getType()
|
||||
if (objExpr && objExpr.originNode)
|
||||
return getNodeValue(objExpr.originNode)
|
||||
return null
|
||||
}
|
||||
return node.value
|
||||
}
|
||||
}
|
||||
|
||||
function getPosition(node) {
|
||||
if (node.callee) {
|
||||
// This is a CallExpression node.
|
||||
// We get the position of the function name.
|
||||
return getPosition(node.callee)
|
||||
}
|
||||
if (node.property) {
|
||||
// This is a MemberExpression node.
|
||||
// We get the position of the property.
|
||||
return node.property
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
function getTypeName(type) {
|
||||
if (!type) return "Unknown type"
|
||||
if (type.types) {
|
||||
// multiple types
|
||||
var types = type.types,
|
||||
s = ""
|
||||
for (var i = 0; i < types.length; i++) {
|
||||
if (i > 0) s += "|"
|
||||
var t = getTypeName(types[i])
|
||||
if (t != "Unknown type") s += t
|
||||
}
|
||||
return s == "" ? "Unknown type" : s
|
||||
}
|
||||
if (type.name) {
|
||||
return type.name
|
||||
}
|
||||
return type.proto ? type.proto.name : "Unknown type"
|
||||
}
|
||||
|
||||
function hasProto(expectedType, name) {
|
||||
if (!expectedType) return false
|
||||
if (!expectedType.proto) return false
|
||||
return expectedType.proto.name === name
|
||||
}
|
||||
|
||||
function isRegexExpected(expectedType) {
|
||||
return hasProto(expectedType, "RegExp.prototype")
|
||||
}
|
||||
|
||||
function isEmptyType(val) {
|
||||
return !val || (val.types && val.types.length == 0)
|
||||
}
|
||||
|
||||
function compareType(expected, actual) {
|
||||
if (isEmptyType(expected) || isEmptyType(actual)) return true
|
||||
if (expected.types) {
|
||||
for (var i = 0; i < expected.types.length; i++) {
|
||||
if (actual.types) {
|
||||
for (var j = 0; j < actual.types.length; j++) {
|
||||
if (compareType(expected.types[i], actual.types[j])) return true
|
||||
}
|
||||
} else {
|
||||
if (compareType(expected.types[i], actual.getType())) return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
} else if (actual.types) {
|
||||
for (var i = 0; i < actual.types.length; i++) {
|
||||
if (compareType(expected.getType(), actual.types[i])) return true
|
||||
}
|
||||
}
|
||||
var expectedType = expected.getType(),
|
||||
actualType = actual.getType()
|
||||
if (!expectedType || !actualType) return true
|
||||
var currentProto = actualType.proto
|
||||
while (currentProto) {
|
||||
if (expectedType.proto && expectedType.proto.name === currentProto.name)
|
||||
return true
|
||||
currentProto = currentProto.proto
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function checkPropsInObject(node, expectedArg, actualObj, invalidArgument) {
|
||||
var properties = node.properties,
|
||||
expectedObj = expectedArg.getType()
|
||||
for (var i = 0; i < properties.length; i++) {
|
||||
var property = properties[i],
|
||||
key = property.key,
|
||||
prop = key && key.name,
|
||||
value = property.value
|
||||
if (prop) {
|
||||
var expectedType = expectedObj.hasProp(prop)
|
||||
if (!expectedType) {
|
||||
// key doesn't exists
|
||||
addMessage(
|
||||
key,
|
||||
"Invalid property at " +
|
||||
(i + 1) +
|
||||
": " +
|
||||
prop +
|
||||
" is not a property in " +
|
||||
getTypeName(expectedArg),
|
||||
invalidArgument.severity
|
||||
)
|
||||
} else {
|
||||
// test that each object literal prop is the correct type
|
||||
var actualType = actualObj.props[prop]
|
||||
if (!compareType(expectedType, actualType)) {
|
||||
addMessage(
|
||||
value,
|
||||
"Invalid property at " +
|
||||
(i + 1) +
|
||||
": cannot convert from " +
|
||||
getTypeName(actualType) +
|
||||
" to " +
|
||||
getTypeName(expectedType),
|
||||
invalidArgument.severity
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function checkItemInArray(node, expectedArg, state, invalidArgument) {
|
||||
var elements = node.elements,
|
||||
expectedType = expectedArg.hasProp("<i>")
|
||||
for (var i = 0; i < elements.length; i++) {
|
||||
var elt = elements[i],
|
||||
actualType = infer.expressionType({ node: elt, state: state })
|
||||
if (!compareType(expectedType, actualType)) {
|
||||
addMessage(
|
||||
elt,
|
||||
"Invalid item at " +
|
||||
(i + 1) +
|
||||
": cannot convert from " +
|
||||
getTypeName(actualType) +
|
||||
" to " +
|
||||
getTypeName(expectedType),
|
||||
invalidArgument.severity
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isObjectLiteral(type) {
|
||||
var objType = type.getObjType()
|
||||
return objType && objType.proto && objType.proto.name == "Object.prototype"
|
||||
}
|
||||
|
||||
function getFunctionLint(fnType) {
|
||||
if (fnType.lint) return fnType.lint
|
||||
if (fnType.metaData) {
|
||||
fnType.lint = getLint(fnType.metaData["!lint"])
|
||||
return fnType.lint
|
||||
}
|
||||
}
|
||||
|
||||
function isFunctionType(type) {
|
||||
if (type.types) {
|
||||
for (var i = 0; i < type.types.length; i++) {
|
||||
if (isFunctionType(type.types[i])) return true
|
||||
}
|
||||
}
|
||||
return type.proto && type.proto.name == "Function.prototype"
|
||||
}
|
||||
|
||||
function validateCallExpression(node, state, c) {
|
||||
var notAFunctionRule = getRule("NotAFunction"),
|
||||
invalidArgument = getRule("InvalidArgument")
|
||||
if (!notAFunctionRule && !invalidArgument) return
|
||||
var type = infer.expressionType({ node: node.callee, state: state })
|
||||
if (!type.isEmpty()) {
|
||||
// If type.isEmpty(), it is handled by MemberExpression/Identifier already.
|
||||
|
||||
// An expression can have multiple possible (guessed) types.
|
||||
// If one of them is a function, type.getFunctionType() will return it.
|
||||
var fnType = type.getFunctionType()
|
||||
if (fnType == null) {
|
||||
if (notAFunctionRule && !isFunctionType(type))
|
||||
addMessage(
|
||||
node,
|
||||
"'" + getNodeName(node) + "' is not a function",
|
||||
notAFunctionRule.severity
|
||||
)
|
||||
return
|
||||
}
|
||||
var fnLint = getFunctionLint(fnType)
|
||||
var continueLint = fnLint ? fnLint(node, addMessage, getRule) : true
|
||||
if (continueLint && fnType.args) {
|
||||
// validate parameters of the function
|
||||
if (!invalidArgument) return
|
||||
var actualArgs = node.arguments
|
||||
if (!actualArgs) return
|
||||
var expectedArgs = fnType.args
|
||||
for (var i = 0; i < expectedArgs.length; i++) {
|
||||
var expectedArg = expectedArgs[i]
|
||||
if (actualArgs.length > i) {
|
||||
var actualNode = actualArgs[i]
|
||||
if (isRegexExpected(expectedArg.getType())) {
|
||||
var value = getNodeValue(actualNode)
|
||||
if (value) {
|
||||
try {
|
||||
var regex = new RegExp(value)
|
||||
} catch (e) {
|
||||
addMessage(
|
||||
actualNode,
|
||||
"Invalid argument at " + (i + 1) + ": " + e,
|
||||
invalidArgument.severity
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var actualArg = infer.expressionType({
|
||||
node: actualNode,
|
||||
state: state,
|
||||
})
|
||||
// if actual type is an Object literal and expected type is an object, we ignore
|
||||
// the comparison type since object literal properties validation is done inside "ObjectExpression".
|
||||
if (!(expectedArg.getObjType() && isObjectLiteral(actualArg))) {
|
||||
if (!compareType(expectedArg, actualArg)) {
|
||||
addMessage(
|
||||
actualNode,
|
||||
"Invalid argument at " +
|
||||
(i + 1) +
|
||||
": cannot convert from " +
|
||||
getTypeName(actualArg) +
|
||||
" to " +
|
||||
getTypeName(expectedArg),
|
||||
invalidArgument.severity
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateAssignement(nodeLeft, nodeRight, rule, state) {
|
||||
if (!nodeLeft || !nodeRight) return
|
||||
if (!rule) return
|
||||
var leftType = infer.expressionType({ node: nodeLeft, state: state }),
|
||||
rightType = infer.expressionType({ node: nodeRight, state: state })
|
||||
if (!compareType(leftType, rightType)) {
|
||||
addMessage(
|
||||
nodeRight,
|
||||
"Type mismatch: cannot convert from " +
|
||||
getTypeName(leftType) +
|
||||
" to " +
|
||||
getTypeName(rightType),
|
||||
rule.severity
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function validateDeclaration(node, state, c) {
|
||||
function isUsedVariable(varNode, varState, file, srv) {
|
||||
var name = varNode.name
|
||||
|
||||
for (
|
||||
var scope = varState;
|
||||
scope && !(name in scope.props);
|
||||
scope = scope.prev
|
||||
) {}
|
||||
if (!scope) return false
|
||||
|
||||
var hasRef = false
|
||||
function searchRef(file) {
|
||||
return function (node, scopeHere) {
|
||||
if (node != varNode) {
|
||||
hasRef = true
|
||||
throw new Error() // throw an error to stop the search.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (scope.node) {
|
||||
// local scope
|
||||
infer.findRefs(scope.node, scope, name, scope, searchRef(file))
|
||||
} else {
|
||||
// global scope
|
||||
infer.findRefs(file.ast, file.scope, name, scope, searchRef(file))
|
||||
for (var i = 0; i < srv.files.length && !hasRef; ++i) {
|
||||
var cur = srv.files[i]
|
||||
if (cur != file)
|
||||
infer.findRefs(cur.ast, cur.scope, name, scope, searchRef(cur))
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
return hasRef
|
||||
}
|
||||
|
||||
var unusedRule = getRule("UnusedVariable"),
|
||||
mismatchRule = getRule("TypeMismatch")
|
||||
if (!unusedRule && !mismatchRule) return
|
||||
switch (node.type) {
|
||||
case "VariableDeclaration":
|
||||
for (var i = 0; i < node.declarations.length; ++i) {
|
||||
var decl = node.declarations[i],
|
||||
varNode = decl.id
|
||||
if (varNode.name != "✖") {
|
||||
// unused variable
|
||||
if (unusedRule && !isUsedVariable(varNode, state, file, server))
|
||||
addMessage(
|
||||
varNode,
|
||||
"Unused variable '" + getNodeName(varNode) + "'",
|
||||
unusedRule.severity
|
||||
)
|
||||
// type mismatch?
|
||||
if (mismatchRule)
|
||||
validateAssignement(varNode, decl.init, mismatchRule, state)
|
||||
}
|
||||
}
|
||||
break
|
||||
case "FunctionDeclaration":
|
||||
if (unusedRule) {
|
||||
var varNode = node.id
|
||||
if (
|
||||
varNode.name != "✖" &&
|
||||
!isUsedVariable(varNode, state, file, server)
|
||||
)
|
||||
addMessage(
|
||||
varNode,
|
||||
"Unused function '" + getNodeName(varNode) + "'",
|
||||
unusedRule.severity
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function getArrType(type) {
|
||||
if (type instanceof infer.Arr) {
|
||||
return type.getObjType()
|
||||
} else if (type.types) {
|
||||
for (var i = 0; i < type.types.length; i++) {
|
||||
if (getArrType(type.types[i])) return type.types[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var visitors = {
|
||||
VariableDeclaration: validateDeclaration,
|
||||
FunctionDeclaration: validateDeclaration,
|
||||
ReturnStatement: function (node, state, c) {
|
||||
if (!node.argument) return
|
||||
var rule = getRule("MixedReturnTypes")
|
||||
if (!rule) return
|
||||
if (state.fnType && state.fnType.retval) {
|
||||
var actualType = infer.expressionType({
|
||||
node: node.argument,
|
||||
state: state,
|
||||
}),
|
||||
expectedType = state.fnType.retval
|
||||
if (!compareType(expectedType, actualType)) {
|
||||
addMessage(
|
||||
node,
|
||||
"Invalid return type : cannot convert from " +
|
||||
getTypeName(actualType) +
|
||||
" to " +
|
||||
getTypeName(expectedType),
|
||||
rule.severity
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
// Detects expressions of the form `object.property`
|
||||
MemberExpression: function (node, state, c) {
|
||||
var rule = getRule("UnknownProperty")
|
||||
if (!rule) return
|
||||
var prop = node.property && node.property.name
|
||||
if (!prop || prop == "✖") return
|
||||
var type = infer.expressionType({ node: node, state: state })
|
||||
var parentType = infer.expressionType({ node: node.object, state: state })
|
||||
|
||||
if (node.computed) {
|
||||
// Bracket notation.
|
||||
// Until we figure out how to handle these properly, we ignore these nodes.
|
||||
return
|
||||
}
|
||||
|
||||
if (!parentType.isEmpty() && type.isEmpty()) {
|
||||
// The type of the property cannot be determined, which means
|
||||
// that the property probably doesn't exist.
|
||||
|
||||
// We only do this check if the parent type is known,
|
||||
// otherwise we will generate errors for an entire chain of unknown
|
||||
// properties.
|
||||
|
||||
// Also, the expression may be valid even if the parent type is unknown,
|
||||
// since the inference engine cannot detect the type in all cases.
|
||||
|
||||
var propertyDefined = false
|
||||
|
||||
// In some cases the type is unknown, even if the property is defined
|
||||
if (parentType.types) {
|
||||
// We cannot use parentType.hasProp or parentType.props - in the case of an AVal,
|
||||
// this may contain properties that are not really defined.
|
||||
parentType.types.forEach(function (potentialType) {
|
||||
// Obj#hasProp checks the prototype as well
|
||||
if (
|
||||
typeof potentialType.hasProp == "function" &&
|
||||
potentialType.hasProp(prop, true)
|
||||
) {
|
||||
propertyDefined = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (!propertyDefined) {
|
||||
addMessage(
|
||||
node,
|
||||
"Unknown property '" + getNodeName(node) + "'",
|
||||
rule.severity
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
// Detects top-level identifiers, e.g. the object in
|
||||
// `object.property` or just `object`.
|
||||
Identifier: function (node, state, c) {
|
||||
var rule = getRule("UnknownIdentifier")
|
||||
if (!rule) return
|
||||
var type = infer.expressionType({ node: node, state: state })
|
||||
|
||||
if (type.originNode != null || type.origin != null) {
|
||||
// The node is defined somewhere (could be this node),
|
||||
// regardless of whether or not the type is known.
|
||||
} else if (type.isEmpty()) {
|
||||
// The type of the identifier cannot be determined,
|
||||
// and the origin is unknown.
|
||||
addMessage(
|
||||
node,
|
||||
"Unknown identifier '" + getNodeName(node) + "'",
|
||||
rule.severity
|
||||
)
|
||||
} else {
|
||||
// Even though the origin node is unknown, the type is known.
|
||||
// This is typically the case for built-in identifiers (e.g. window or document).
|
||||
}
|
||||
},
|
||||
// Detects function calls.
|
||||
// `node.callee` is the expression (Identifier or MemberExpression)
|
||||
// the is called as a function.
|
||||
NewExpression: validateCallExpression,
|
||||
CallExpression: validateCallExpression,
|
||||
AssignmentExpression: function (node, state, c) {
|
||||
var rule = getRule("TypeMismatch")
|
||||
validateAssignement(node.left, node.right, rule, state)
|
||||
},
|
||||
ObjectExpression: function (node, state, c) {
|
||||
// validate properties of the object literal
|
||||
var rule = getRule("ObjectLiteral")
|
||||
if (!rule) return
|
||||
var actualType = node.objType
|
||||
var ctxType = infer.typeFromContext(file.ast, {
|
||||
node: node,
|
||||
state: state,
|
||||
}),
|
||||
expectedType = null
|
||||
if (ctxType instanceof infer.Obj) {
|
||||
expectedType = ctxType.getObjType()
|
||||
} else if (ctxType && ctxType.makeupType) {
|
||||
var objType = ctxType.makeupType()
|
||||
if (objType && objType.getObjType()) {
|
||||
expectedType = objType.getObjType()
|
||||
}
|
||||
}
|
||||
if (expectedType && expectedType != actualType) {
|
||||
// expected type is known. Ex: config object of RequireJS
|
||||
checkPropsInObject(node, expectedType, actualType, rule)
|
||||
}
|
||||
},
|
||||
ArrayExpression: function (node, state, c) {
|
||||
// validate elements of the Arrray
|
||||
var rule = getRule("Array")
|
||||
if (!rule) return
|
||||
//var actualType = infer.expressionType({node: node, state: state});
|
||||
var ctxType = infer.typeFromContext(file.ast, {
|
||||
node: node,
|
||||
state: state,
|
||||
}),
|
||||
expectedType = getArrType(ctxType)
|
||||
if (expectedType /*&& expectedType != actualType*/) {
|
||||
// expected type is known. Ex: config object of RequireJS
|
||||
checkItemInArray(node, expectedType, state, rule)
|
||||
}
|
||||
},
|
||||
ImportDeclaration: function (node, state, c) {
|
||||
// Validate ES6 modules from + specifiers
|
||||
var rule = getRule("ES6Modules")
|
||||
if (!rule) return
|
||||
var me = infer.cx().parent.mod.modules
|
||||
if (!me) return // tern plugin modules.js is not loaded
|
||||
var source = node.source
|
||||
if (!source) return
|
||||
// Validate ES6 modules "from"
|
||||
var modType = me.getModType(source)
|
||||
if (!modType) {
|
||||
addMessage(
|
||||
source,
|
||||
"Invalid modules from '" + source.value + "'",
|
||||
rule.severity
|
||||
)
|
||||
return
|
||||
}
|
||||
// Validate ES6 modules "specifiers"
|
||||
var specifiers = node.specifiers,
|
||||
specifier
|
||||
if (!specifiers) return
|
||||
for (var i = 0; i < specifiers.length; i++) {
|
||||
var specifier = specifiers[i],
|
||||
imported = specifier.imported
|
||||
if (imported) {
|
||||
var name = imported.name
|
||||
if (!modType.hasProp(name))
|
||||
addMessage(
|
||||
imported,
|
||||
"Invalid modules specifier '" + getNodeName(imported) + "'",
|
||||
rule.severity
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return visitors
|
||||
}
|
||||
|
||||
// Adapted from infer.searchVisitor.
|
||||
// Record the scope and pass it through in the state.
|
||||
// VariableDeclaration in infer.searchVisitor breaks things for us.
|
||||
var scopeVisitor = walk.make({
|
||||
Function: function (node, _st, c) {
|
||||
var scope = node.scope
|
||||
if (node.id) c(node.id, scope)
|
||||
for (var i = 0; i < node.params.length; ++i) c(node.params[i], scope)
|
||||
c(node.body, scope, "ScopeBody")
|
||||
},
|
||||
Statement: function (node, st, c) {
|
||||
c(node, node.scope || st)
|
||||
},
|
||||
})
|
||||
|
||||
// Validate one file
|
||||
|
||||
export function validateFile(server, query, file) {
|
||||
try {
|
||||
var messages = [],
|
||||
ast = file.ast,
|
||||
state = file.scope
|
||||
var visitors = makeVisitors(server, query, file, messages)
|
||||
walk.simple(ast, visitors, infer.searchVisitor, state)
|
||||
return { messages: messages }
|
||||
} catch (e) {
|
||||
console.error(e.stack)
|
||||
return { messages: [] }
|
||||
}
|
||||
}
|
||||
|
||||
export function registerTernLinter() {
|
||||
tern.defineQueryType("lint", {
|
||||
takesFile: true,
|
||||
run: function (server, query, file) {
|
||||
return validateFile(server, query, file)
|
||||
},
|
||||
})
|
||||
|
||||
tern.defineQueryType("lint-full", {
|
||||
run: function (server, query) {
|
||||
return validateFiles(server, query)
|
||||
},
|
||||
})
|
||||
|
||||
tern.registerPlugin("lint", function (server, options) {
|
||||
server._lint = {
|
||||
rules: getRules(options),
|
||||
}
|
||||
return {
|
||||
passes: {},
|
||||
loadFirst: true,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Validate the whole files of the server
|
||||
|
||||
export function validateFiles(server, query) {
|
||||
try {
|
||||
var messages = [],
|
||||
files = server.files,
|
||||
groupByFiles = query.groupByFiles == true
|
||||
for (var i = 0; i < files.length; ++i) {
|
||||
var messagesFile = groupByFiles ? [] : messages,
|
||||
file = files[i],
|
||||
ast = file.ast,
|
||||
state = file.scope
|
||||
var visitors = makeVisitors(server, query, file, messagesFile)
|
||||
walk.simple(ast, visitors, infer.searchVisitor, state)
|
||||
if (groupByFiles)
|
||||
messages.push({ file: file.name, messages: messagesFile })
|
||||
}
|
||||
return { messages: messages }
|
||||
} catch (e) {
|
||||
console.error(e.stack)
|
||||
return { messages: [] }
|
||||
}
|
||||
}
|
||||
|
||||
var lints = Object.create(null)
|
||||
|
||||
var getLint = (tern.getLint = function (name) {
|
||||
if (!name) return null
|
||||
return lints[name]
|
||||
})
|
||||
|
||||
function getRules(options) {
|
||||
var rules = {}
|
||||
for (var ruleName in defaultRules) {
|
||||
if (
|
||||
options &&
|
||||
options.rules &&
|
||||
options.rules[ruleName] &&
|
||||
options.rules[ruleName].severity
|
||||
) {
|
||||
if (options.rules[ruleName].severity != "none")
|
||||
rules[ruleName] = options.rules[ruleName]
|
||||
} else {
|
||||
rules[ruleName] = defaultRules[ruleName]
|
||||
}
|
||||
}
|
||||
return rules
|
||||
}
|
||||
|
||||
function getRule(ruleName) {
|
||||
const cx = infer.cx()
|
||||
const server = cx.parent
|
||||
const rules =
|
||||
server && server._lint && server._lint.rules
|
||||
? server._lint.rules
|
||||
: defaultRules
|
||||
return rules[ruleName]
|
||||
}
|
||||
44
packages/hoppscotch-app/helpers/testSnippets.js
Normal file
44
packages/hoppscotch-app/helpers/testSnippets.js
Normal file
@@ -0,0 +1,44 @@
|
||||
export default [
|
||||
{
|
||||
name: "Response: Status code is 200",
|
||||
script: `\n\n// Check status code is 200
|
||||
pw.test("Status code is 200", ()=> {
|
||||
pw.expect(pw.response.status).toBe(200);
|
||||
});`,
|
||||
},
|
||||
{
|
||||
name: "Response: Assert property from body",
|
||||
script: `\n\n// Check JSON response property
|
||||
pw.test("Check JSON response property", ()=> {
|
||||
pw.expect(pw.response.method).toBe("GET");
|
||||
});`,
|
||||
},
|
||||
{
|
||||
name: "Status code: Status code is 2xx",
|
||||
script: `\n\n// Check status code is 2xx
|
||||
pw.test("Status code is 2xx", ()=> {
|
||||
pw.expect(pw.response.status).toBeLevel2xx();
|
||||
});`,
|
||||
},
|
||||
{
|
||||
name: "Status code: Status code is 3xx",
|
||||
script: `\n\n// Check status code is 3xx
|
||||
pw.test("Status code is 3xx", ()=> {
|
||||
pw.expect(pw.response.status).toBeLevel3xx();
|
||||
});`,
|
||||
},
|
||||
{
|
||||
name: "Status code: Status code is 4xx",
|
||||
script: `\n\n// Check status code is 4xx
|
||||
pw.test("Status code is 4xx", ()=> {
|
||||
pw.expect(pw.response.status).toBeLevel4xx();
|
||||
});`,
|
||||
},
|
||||
{
|
||||
name: "Status code: Status code is 5xx",
|
||||
script: `\n\n// Check status code is 5xx
|
||||
pw.test("Status code is 5xx", ()=> {
|
||||
pw.expect(pw.response.status).toBeLevel5xx();
|
||||
});`,
|
||||
},
|
||||
]
|
||||
41
packages/hoppscotch-app/helpers/types/HoppGQLRequest.ts
Normal file
41
packages/hoppscotch-app/helpers/types/HoppGQLRequest.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
export type GQLHeader = {
|
||||
key: string
|
||||
value: string
|
||||
active: boolean
|
||||
}
|
||||
|
||||
export type HoppGQLRequest = {
|
||||
v: number
|
||||
name: string
|
||||
url: string
|
||||
headers: GQLHeader[]
|
||||
query: string
|
||||
variables: string
|
||||
}
|
||||
|
||||
export function translateToGQLRequest(x: any): HoppGQLRequest {
|
||||
if (x.v && x.v === 1) return x
|
||||
|
||||
// Old request
|
||||
const name = x.name ?? "Untitled"
|
||||
const url = x.url ?? ""
|
||||
const headers = x.headers ?? []
|
||||
const query = x.query ?? ""
|
||||
const variables = x.variables ?? []
|
||||
|
||||
return {
|
||||
v: 1,
|
||||
name,
|
||||
url,
|
||||
headers,
|
||||
query,
|
||||
variables,
|
||||
}
|
||||
}
|
||||
|
||||
export function makeGQLRequest(x: Omit<HoppGQLRequest, "v">) {
|
||||
return {
|
||||
v: 1,
|
||||
...x,
|
||||
}
|
||||
}
|
||||
34
packages/hoppscotch-app/helpers/types/HoppRESTAuth.ts
Normal file
34
packages/hoppscotch-app/helpers/types/HoppRESTAuth.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
export type HoppRESTAuthNone = {
|
||||
authType: "none"
|
||||
}
|
||||
|
||||
export type HoppRESTAuthBasic = {
|
||||
authType: "basic"
|
||||
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export type HoppRESTAuthBearer = {
|
||||
authType: "bearer"
|
||||
|
||||
token: string
|
||||
}
|
||||
|
||||
export type HoppRESTAuthOAuth2 = {
|
||||
authType: "oauth-2"
|
||||
|
||||
token: string
|
||||
oidcDiscoveryURL: string
|
||||
authURL: string
|
||||
accessTokenURL: string
|
||||
clientID: string
|
||||
scope: string
|
||||
}
|
||||
|
||||
export type HoppRESTAuth = { authActive: boolean } & (
|
||||
| HoppRESTAuthNone
|
||||
| HoppRESTAuthBasic
|
||||
| HoppRESTAuthBearer
|
||||
| HoppRESTAuthOAuth2
|
||||
)
|
||||
161
packages/hoppscotch-app/helpers/types/HoppRESTRequest.ts
Normal file
161
packages/hoppscotch-app/helpers/types/HoppRESTRequest.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
import { ValidContentTypes } from "../utils/contenttypes"
|
||||
import { HoppRESTAuth } from "./HoppRESTAuth"
|
||||
|
||||
export const RESTReqSchemaVersion = "1"
|
||||
|
||||
export type HoppRESTParam = {
|
||||
key: string
|
||||
value: string
|
||||
active: boolean
|
||||
}
|
||||
|
||||
export type HoppRESTHeader = {
|
||||
key: string
|
||||
value: string
|
||||
active: boolean
|
||||
}
|
||||
|
||||
export type FormDataKeyValue = {
|
||||
key: string
|
||||
active: boolean
|
||||
} & ({ isFile: true; value: Blob[] } | { isFile: false; value: string })
|
||||
|
||||
export type HoppRESTReqBodyFormData = {
|
||||
contentType: "multipart/form-data"
|
||||
body: FormDataKeyValue[]
|
||||
}
|
||||
|
||||
export type HoppRESTReqBody =
|
||||
| {
|
||||
contentType: Exclude<ValidContentTypes, "multipart/form-data">
|
||||
body: string
|
||||
}
|
||||
| HoppRESTReqBodyFormData
|
||||
| {
|
||||
contentType: null
|
||||
body: null
|
||||
}
|
||||
|
||||
export interface HoppRESTRequest {
|
||||
v: string
|
||||
id?: string // Firebase Firestore ID
|
||||
|
||||
name: string
|
||||
method: string
|
||||
endpoint: string
|
||||
params: HoppRESTParam[]
|
||||
headers: HoppRESTHeader[]
|
||||
preRequestScript: string
|
||||
testScript: string
|
||||
|
||||
auth: HoppRESTAuth
|
||||
|
||||
body: HoppRESTReqBody
|
||||
}
|
||||
|
||||
export function makeRESTRequest(
|
||||
x: Omit<HoppRESTRequest, "v">
|
||||
): HoppRESTRequest {
|
||||
return {
|
||||
...x,
|
||||
v: RESTReqSchemaVersion,
|
||||
}
|
||||
}
|
||||
|
||||
export function isHoppRESTRequest(x: any): x is HoppRESTRequest {
|
||||
return x && typeof x === "object" && "v" in x
|
||||
}
|
||||
|
||||
function parseRequestBody(x: any): HoppRESTReqBody {
|
||||
if (x.contentType === "application/json") {
|
||||
return {
|
||||
contentType: "application/json",
|
||||
body: x.rawParams,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
contentType: "application/json",
|
||||
body: "",
|
||||
}
|
||||
}
|
||||
|
||||
export function translateToNewRequest(x: any): HoppRESTRequest {
|
||||
if (isHoppRESTRequest(x)) {
|
||||
return x
|
||||
} else {
|
||||
// Old format
|
||||
const endpoint: string = `${x.url}${x.path}`
|
||||
|
||||
const headers: HoppRESTHeader[] = x.headers ?? []
|
||||
|
||||
// Remove old keys from params
|
||||
const params: HoppRESTParam[] = (x.params ?? []).map(
|
||||
({
|
||||
key,
|
||||
value,
|
||||
active,
|
||||
}: {
|
||||
key: string
|
||||
value: string
|
||||
active: boolean
|
||||
}) => ({
|
||||
key,
|
||||
value,
|
||||
active,
|
||||
})
|
||||
)
|
||||
|
||||
const name = x.name
|
||||
const method = x.method
|
||||
|
||||
const preRequestScript = x.preRequestScript
|
||||
const testScript = x.testScript
|
||||
|
||||
const body = parseRequestBody(x)
|
||||
|
||||
const auth = parseOldAuth(x)
|
||||
|
||||
const result: HoppRESTRequest = {
|
||||
name,
|
||||
endpoint,
|
||||
headers,
|
||||
params,
|
||||
method,
|
||||
preRequestScript,
|
||||
testScript,
|
||||
body,
|
||||
auth,
|
||||
v: RESTReqSchemaVersion,
|
||||
}
|
||||
|
||||
if (x.id) result.id = x.id
|
||||
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
export function parseOldAuth(x: any): HoppRESTAuth {
|
||||
if (!x.auth || x.auth === "None")
|
||||
return {
|
||||
authType: "none",
|
||||
authActive: true,
|
||||
}
|
||||
|
||||
if (x.auth === "Basic Auth")
|
||||
return {
|
||||
authType: "basic",
|
||||
authActive: true,
|
||||
username: x.httpUser,
|
||||
password: x.httpPassword,
|
||||
}
|
||||
|
||||
if (x.auth === "Bearer Token")
|
||||
return {
|
||||
authType: "bearer",
|
||||
authActive: true,
|
||||
token: x.bearerToken,
|
||||
}
|
||||
|
||||
return { authType: "none", authActive: true }
|
||||
}
|
||||
35
packages/hoppscotch-app/helpers/types/HoppRESTResponse.ts
Normal file
35
packages/hoppscotch-app/helpers/types/HoppRESTResponse.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { HoppRESTRequest } from "./HoppRESTRequest"
|
||||
|
||||
export type HoppRESTResponse =
|
||||
| { type: "loading"; req: HoppRESTRequest }
|
||||
| {
|
||||
type: "fail"
|
||||
headers: { key: string; value: string }[]
|
||||
body: ArrayBuffer
|
||||
statusCode: number
|
||||
|
||||
meta: {
|
||||
responseSize: number // in bytes
|
||||
responseDuration: number // in millis
|
||||
}
|
||||
|
||||
req: HoppRESTRequest
|
||||
}
|
||||
| {
|
||||
type: "network_fail"
|
||||
error: Error
|
||||
|
||||
req: HoppRESTRequest
|
||||
}
|
||||
| {
|
||||
type: "success"
|
||||
headers: { key: string; value: string }[]
|
||||
body: ArrayBuffer
|
||||
statusCode: number
|
||||
meta: {
|
||||
responseSize: number // in bytes
|
||||
responseDuration: number // in millis
|
||||
}
|
||||
|
||||
req: HoppRESTRequest
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* We use the save context to figure out
|
||||
* how a loaded request is to be saved.
|
||||
* These will be set when the request is loaded
|
||||
* into the request session (RESTSession)
|
||||
*/
|
||||
export type HoppRequestSaveContext =
|
||||
| {
|
||||
/**
|
||||
* The origin source of the request
|
||||
*/
|
||||
originLocation: "user-collection"
|
||||
/**
|
||||
* Path to the request folder
|
||||
*/
|
||||
folderPath: string
|
||||
/**
|
||||
* Index to the request
|
||||
*/
|
||||
requestIndex: number
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* The origin source of the request
|
||||
*/
|
||||
originLocation: "team-collection"
|
||||
/**
|
||||
* ID of the request in the team
|
||||
*/
|
||||
requestID: string
|
||||
}
|
||||
15
packages/hoppscotch-app/helpers/types/HoppTestResult.ts
Normal file
15
packages/hoppscotch-app/helpers/types/HoppTestResult.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export type HoppTestExpectResult = {
|
||||
status: "fail" | "pass" | "error"
|
||||
message: string
|
||||
}
|
||||
|
||||
export type HoppTestData = {
|
||||
description: string
|
||||
expectResults: HoppTestExpectResult[]
|
||||
tests: HoppTestData[]
|
||||
}
|
||||
|
||||
export type HoppTestResult = {
|
||||
tests: HoppTestData[]
|
||||
expectResults: HoppTestExpectResult[]
|
||||
}
|
||||
149
packages/hoppscotch-app/helpers/utils/EffectiveURL.ts
Normal file
149
packages/hoppscotch-app/helpers/utils/EffectiveURL.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { combineLatest, Observable } from "rxjs"
|
||||
import { map } from "rxjs/operators"
|
||||
import { FormDataKeyValue, HoppRESTRequest } from "../types/HoppRESTRequest"
|
||||
import parseTemplateString from "../templating"
|
||||
import { Environment, getGlobalVariables } from "~/newstore/environments"
|
||||
|
||||
export interface EffectiveHoppRESTRequest extends HoppRESTRequest {
|
||||
/**
|
||||
* The effective final URL.
|
||||
*
|
||||
* This contains path, params and environment variables all applied to it
|
||||
*/
|
||||
effectiveFinalURL: string
|
||||
effectiveFinalHeaders: { key: string; value: string }[]
|
||||
effectiveFinalParams: { key: string; value: string }[]
|
||||
effectiveFinalBody: FormData | string | null
|
||||
}
|
||||
|
||||
function getFinalBodyFromRequest(
|
||||
request: HoppRESTRequest,
|
||||
env: Environment
|
||||
): FormData | string | null {
|
||||
if (request.body.contentType === null) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (request.body.contentType === "multipart/form-data") {
|
||||
const formData = new FormData()
|
||||
|
||||
request.body.body
|
||||
.filter((x) => x.key !== "" && x.active) // Remove empty keys
|
||||
.map(
|
||||
(x) =>
|
||||
<FormDataKeyValue>{
|
||||
active: x.active,
|
||||
isFile: x.isFile,
|
||||
key: parseTemplateString(x.key, env.variables),
|
||||
value: x.isFile
|
||||
? x.value
|
||||
: parseTemplateString(x.value, env.variables),
|
||||
}
|
||||
)
|
||||
.forEach((entry) => {
|
||||
if (!entry.isFile) formData.append(entry.key, entry.value)
|
||||
else entry.value.forEach((blob) => formData.append(entry.key, blob))
|
||||
})
|
||||
|
||||
return formData
|
||||
} else return request.body.body
|
||||
}
|
||||
|
||||
/**
|
||||
* Outputs an executable request format with environment variables applied
|
||||
*
|
||||
* @param request The request to source from
|
||||
* @param environment The environment to apply
|
||||
*
|
||||
* @returns An object with extra fields defining a complete request
|
||||
*/
|
||||
export function getEffectiveRESTRequest(
|
||||
request: HoppRESTRequest,
|
||||
environment: Environment
|
||||
): EffectiveHoppRESTRequest {
|
||||
const envVariables = [...environment.variables, ...getGlobalVariables()]
|
||||
|
||||
const effectiveFinalHeaders = request.headers
|
||||
.filter(
|
||||
(x) =>
|
||||
x.key !== "" && // Remove empty keys
|
||||
x.active // Only active
|
||||
)
|
||||
.map((x) => ({
|
||||
// Parse out environment template strings
|
||||
active: true,
|
||||
key: parseTemplateString(x.key, envVariables),
|
||||
value: parseTemplateString(x.value, envVariables),
|
||||
}))
|
||||
|
||||
// Authentication
|
||||
if (request.auth.authActive) {
|
||||
// TODO: Support a better b64 implementation than btoa ?
|
||||
if (request.auth.authType === "basic") {
|
||||
const username = parseTemplateString(request.auth.username, envVariables)
|
||||
const password = parseTemplateString(request.auth.password, envVariables)
|
||||
|
||||
effectiveFinalHeaders.push({
|
||||
active: true,
|
||||
key: "Authorization",
|
||||
value: `Basic ${btoa(`${username}:${password}`)}`,
|
||||
})
|
||||
} else if (
|
||||
request.auth.authType === "bearer" ||
|
||||
request.auth.authType === "oauth-2"
|
||||
) {
|
||||
effectiveFinalHeaders.push({
|
||||
active: true,
|
||||
key: "Authorization",
|
||||
value: `Bearer ${parseTemplateString(
|
||||
request.auth.token,
|
||||
envVariables
|
||||
)}`,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const effectiveFinalBody = getFinalBodyFromRequest(request, environment)
|
||||
if (request.body.contentType)
|
||||
effectiveFinalHeaders.push({
|
||||
active: true,
|
||||
key: "content-type",
|
||||
value: request.body.contentType,
|
||||
})
|
||||
|
||||
return {
|
||||
...request,
|
||||
effectiveFinalURL: parseTemplateString(request.endpoint, envVariables),
|
||||
effectiveFinalHeaders,
|
||||
effectiveFinalParams: request.params
|
||||
.filter(
|
||||
(x) =>
|
||||
x.key !== "" && // Remove empty keys
|
||||
x.active // Only active
|
||||
)
|
||||
.map((x) => ({
|
||||
active: true,
|
||||
key: parseTemplateString(x.key, envVariables),
|
||||
value: parseTemplateString(x.value, envVariables),
|
||||
})),
|
||||
effectiveFinalBody,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an Observable Stream that emits HoppRESTRequests whenever
|
||||
* the input streams emit a value
|
||||
*
|
||||
* @param request$ The request stream containing request data
|
||||
* @param environment$ The environment stream containing environment data to apply
|
||||
*
|
||||
* @returns Observable Stream for the Effective Request Object
|
||||
*/
|
||||
export function getEffectiveRESTRequestStream(
|
||||
request$: Observable<HoppRESTRequest>,
|
||||
environment$: Observable<Environment>
|
||||
): Observable<EffectiveHoppRESTRequest> {
|
||||
return combineLatest([request$, environment$]).pipe(
|
||||
map(([request, env]) => getEffectiveRESTRequest(request, env))
|
||||
)
|
||||
}
|
||||
24
packages/hoppscotch-app/helpers/utils/StreamUtils.ts
Normal file
24
packages/hoppscotch-app/helpers/utils/StreamUtils.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { combineLatest, Observable } from "rxjs"
|
||||
import { map } from "rxjs/operators"
|
||||
|
||||
/**
|
||||
* Constructs a stream of a object from a collection of other observables
|
||||
*
|
||||
* @param streamObj The object containing key of observables to assemble from
|
||||
*
|
||||
* @returns The constructed object observable
|
||||
*/
|
||||
export function constructFromStreams<T>(
|
||||
streamObj: { [key in keyof T]: Observable<T[key]> }
|
||||
): Observable<T> {
|
||||
return combineLatest(Object.values<Observable<T[keyof T]>>(streamObj)).pipe(
|
||||
map((streams) => {
|
||||
const keys = Object.keys(streamObj) as (keyof T)[]
|
||||
|
||||
return keys.reduce(
|
||||
(acc, s, i) => Object.assign(acc, { [s]: streams[i] }),
|
||||
{}
|
||||
) as T
|
||||
})
|
||||
)
|
||||
}
|
||||
15
packages/hoppscotch-app/helpers/utils/__tests__/b64.spec.js
Normal file
15
packages/hoppscotch-app/helpers/utils/__tests__/b64.spec.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import { TextDecoder } from "util"
|
||||
import { decodeB64StringToArrayBuffer } from "../b64"
|
||||
|
||||
describe("decodeB64StringToArrayBuffer", () => {
|
||||
test("decodes content correctly", () => {
|
||||
const decoder = new TextDecoder("utf-8")
|
||||
expect(
|
||||
decoder.decode(
|
||||
decodeB64StringToArrayBuffer("aG9wcHNjb3RjaCBpcyBhd2Vzb21lIQ==")
|
||||
)
|
||||
).toMatch("hoppscotch is awesome!")
|
||||
})
|
||||
|
||||
// TODO : More tests for binary data ?
|
||||
})
|
||||
@@ -0,0 +1,40 @@
|
||||
import { isJSONContentType } from "../contenttypes"
|
||||
|
||||
describe("isJSONContentType", () => {
|
||||
test("returns true for JSON content types", () => {
|
||||
expect(isJSONContentType("application/json")).toBe(true)
|
||||
expect(isJSONContentType("application/vnd.api+json")).toBe(true)
|
||||
expect(isJSONContentType("application/hal+json")).toBe(true)
|
||||
expect(isJSONContentType("application/ld+json")).toBe(true)
|
||||
})
|
||||
|
||||
test("returns true for JSON types with charset specified", () => {
|
||||
expect(isJSONContentType("application/json; charset=utf-8")).toBe(true)
|
||||
expect(isJSONContentType("application/vnd.api+json; charset=utf-8")).toBe(
|
||||
true
|
||||
)
|
||||
expect(isJSONContentType("application/hal+json; charset=utf-8")).toBe(true)
|
||||
expect(isJSONContentType("application/ld+json; charset=utf-8")).toBe(true)
|
||||
})
|
||||
|
||||
test("returns false for non-JSON content types", () => {
|
||||
expect(isJSONContentType("application/xml")).toBe(false)
|
||||
expect(isJSONContentType("text/html")).toBe(false)
|
||||
expect(isJSONContentType("application/x-www-form-urlencoded")).toBe(false)
|
||||
expect(isJSONContentType("foo/jsoninword")).toBe(false)
|
||||
})
|
||||
|
||||
test("returns false for non-JSON content types with charset", () => {
|
||||
expect(isJSONContentType("application/xml; charset=utf-8")).toBe(false)
|
||||
expect(isJSONContentType("text/html; charset=utf-8")).toBe(false)
|
||||
expect(
|
||||
isJSONContentType("application/x-www-form-urlencoded; charset=utf-8")
|
||||
).toBe(false)
|
||||
expect(isJSONContentType("foo/jsoninword; charset=utf-8")).toBe(false)
|
||||
})
|
||||
|
||||
test("returns false for null/undefined", () => {
|
||||
expect(isJSONContentType(null)).toBe(false)
|
||||
expect(isJSONContentType(undefined)).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,38 @@
|
||||
import debounce from "../debounce"
|
||||
|
||||
describe("debounce", () => {
|
||||
test("doesn't call function right after calling", () => {
|
||||
const fn = jest.fn()
|
||||
|
||||
const debFunc = debounce(fn, 100)
|
||||
debFunc()
|
||||
|
||||
expect(fn).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("calls the function after the given timeout", () => {
|
||||
const fn = jest.fn()
|
||||
|
||||
jest.useFakeTimers()
|
||||
|
||||
const debFunc = debounce(fn, 100)
|
||||
debFunc()
|
||||
|
||||
jest.runAllTimers()
|
||||
|
||||
expect(fn).toHaveBeenCalled()
|
||||
// expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 100)
|
||||
})
|
||||
|
||||
test("calls the function only one time within the timeframe", () => {
|
||||
const fn = jest.fn()
|
||||
|
||||
const debFunc = debounce(fn, 1000)
|
||||
|
||||
for (let i = 0; i < 100; i++) debFunc()
|
||||
|
||||
jest.runAllTimers()
|
||||
|
||||
expect(fn).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
21
packages/hoppscotch-app/helpers/utils/__tests__/uri.spec.js
Normal file
21
packages/hoppscotch-app/helpers/utils/__tests__/uri.spec.js
Normal file
@@ -0,0 +1,21 @@
|
||||
import { parseUrlAndPath } from "../uri"
|
||||
|
||||
describe("parseUrlAndPath", () => {
|
||||
test("has url and path fields", () => {
|
||||
const result = parseUrlAndPath("https://hoppscotch.io/")
|
||||
|
||||
expect(result).toHaveProperty("url")
|
||||
expect(result).toHaveProperty("path")
|
||||
})
|
||||
|
||||
test("parses out URL correctly", () => {
|
||||
const result = parseUrlAndPath("https://hoppscotch.io/test/page")
|
||||
|
||||
expect(result.url).toBe("https://hoppscotch.io")
|
||||
})
|
||||
test("parses out Path correctly", () => {
|
||||
const result = parseUrlAndPath("https://hoppscotch.io/test/page")
|
||||
|
||||
expect(result.path).toBe("/test/page")
|
||||
})
|
||||
})
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user