chore: merge hoppscotch/release/2023.8.3 into hoppscotch/release/2023.12.0

This commit is contained in:
Andrew Bastin
2023-11-03 10:12:54 +05:30
27 changed files with 3039 additions and 1547 deletions

View File

@@ -17,12 +17,12 @@
"types": "dist/index.d.ts",
"sideEffects": false,
"dependencies": {
"@codemirror/language": "^6.9.0",
"@codemirror/language": "^6.9.1",
"@lezer/highlight": "^1.1.6",
"@lezer/lr": "^1.3.10"
"@lezer/lr": "^1.3.13"
},
"devDependencies": {
"@lezer/generator": "^1.5.0",
"@lezer/generator": "^1.5.1",
"mocha": "^9.2.2",
"rollup": "^3.29.3",
"rollup-plugin-dts": "^6.0.2",

View File

@@ -17,22 +17,22 @@
"postinstall": "pnpm run gql-codegen",
"do-test": "pnpm run test",
"do-lint": "pnpm run prod-lint",
"do-typecheck": "pnpm run lint",
"do-typecheck": "node type-check.mjs",
"do-lintfix": "pnpm run lintfix"
},
"dependencies": {
"@apidevtools/swagger-parser": "^10.1.0",
"@codemirror/autocomplete": "^6.9.0",
"@codemirror/commands": "^6.2.4",
"@codemirror/lang-javascript": "^6.1.9",
"@codemirror/autocomplete": "^6.10.2",
"@codemirror/commands": "^6.3.0",
"@codemirror/lang-javascript": "^6.2.1",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-xml": "^6.0.2",
"@codemirror/language": "^6.9.0",
"@codemirror/language": "^6.9.1",
"@codemirror/legacy-modes": "^6.3.3",
"@codemirror/lint": "^6.4.0",
"@codemirror/search": "^6.5.1",
"@codemirror/state": "^6.2.1",
"@codemirror/view": "^6.16.0",
"@codemirror/lint": "^6.4.2",
"@codemirror/search": "^6.5.4",
"@codemirror/state": "^6.3.1",
"@codemirror/view": "^6.21.3",
"@fontsource-variable/inter": "^5.0.8",
"@fontsource-variable/material-symbols-rounded": "^5.0.7",
"@fontsource-variable/roboto-mono": "^5.0.9",
@@ -42,8 +42,6 @@
"@hoppscotch/ui": "workspace:^",
"@hoppscotch/vue-toasted": "^0.1.0",
"@lezer/highlight": "^1.1.6",
"@sentry/tracing": "^7.64.0",
"@sentry/vue": "^7.64.0",
"@urql/core": "^4.1.1",
"@urql/devtools": "^2.0.3",
"@urql/exchange-auth": "^2.1.6",
@@ -139,6 +137,7 @@
"eslint": "^8.47.0",
"eslint-plugin-prettier": "^5.0.0",
"eslint-plugin-vue": "^9.17.0",
"glob": "^10.3.10",
"npm-run-all": "^4.1.5",
"postcss": "^8.4.23",
"prettier-plugin-tailwindcss": "^0.5.6",

View File

@@ -58,7 +58,13 @@ export const FALLBACK_LANG = pipe(
)
// A reference to the i18n instance
let i18nInstance: I18n<any, any, any> | null = null
let i18nInstance: I18n<
Record<string, unknown>,
Record<string, unknown>,
Record<string, unknown>,
string,
true
> | null = null
const resolveCurrentLocale = () =>
pipe(
@@ -119,7 +125,6 @@ export const changeAppLanguage = async (locale: string) => {
* Returns the i18n instance
*/
export function getI18n() {
// @ts-expect-error Something weird with the i18n errors
return i18nInstance!.global.t
}

View File

@@ -1,200 +0,0 @@
import { HoppModule } from "."
import * as Sentry from "@sentry/vue"
import { BrowserTracing } from "@sentry/tracing"
import { Route } from "@sentry/vue/types/router"
import { RouteLocationNormalized, Router } from "vue-router"
import { settingsStore } from "~/newstore/settings"
import { App } from "vue"
import { APP_IS_IN_DEV_MODE } from "~/helpers/dev"
import { gqlClientError$ } from "~/helpers/backend/GQLClient"
import { platform } from "~/platform"
/**
* The tag names we allow giving to Sentry
*/
type SentryTag = "BACKEND_OPERATIONS"
interface SentryVueRouter {
onError: (fn: (err: Error) => void) => void
beforeEach: (fn: (to: Route, from: Route, next: () => void) => void) => void
}
function normalizedRouteToSentryRoute(route: RouteLocationNormalized): Route {
return {
matched: route.matched,
// route.params' type translates just to a fancy version of this, hence assertion
params: route.params as Route["params"],
path: route.path,
// route.query's type translates just to a fancy version of this, hence assertion
query: route.query as Route["query"],
name: route.name,
}
}
function getInstrumentationVueRouter(router: Router): SentryVueRouter {
return <SentryVueRouter>{
onError: router.onError,
beforeEach(func) {
router.beforeEach((to, from, next) => {
func(
normalizedRouteToSentryRoute(to),
normalizedRouteToSentryRoute(from),
next
)
})
},
}
}
let sentryActive = false
function initSentry(dsn: string, router: Router, app: App) {
Sentry.init({
app,
dsn,
release: import.meta.env.VITE_SENTRY_RELEASE_TAG ?? undefined,
environment: APP_IS_IN_DEV_MODE
? "dev"
: import.meta.env.VITE_SENTRY_ENVIRONMENT,
integrations: [
new BrowserTracing({
routingInstrumentation: Sentry.vueRouterInstrumentation(
getInstrumentationVueRouter(router)
),
// TODO: We may want to limit this later on
tracingOrigins: [new URL(import.meta.env.VITE_BACKEND_GQL_URL).origin],
}),
],
tracesSampleRate: 0.8,
})
sentryActive = true
}
function deinitSentry() {
Sentry.close()
sentryActive = false
}
/**
* Reports a set of related errors to Sentry
* @param errs The errors to report
* @param tag The tag for the errord
* @param extraTags Additional tag data to add
* @param extras Extra information to attach
*/
function reportErrors(
errs: Error[],
tag: SentryTag,
extraTags: Record<string, string | number | boolean> | null = null,
extras: any = undefined
) {
if (sentryActive) {
Sentry.withScope((scope) => {
scope.setTag("tag", tag)
if (extraTags) {
Object.entries(extraTags).forEach(([key, value]) => {
scope.setTag(key, value)
})
}
if (extras !== null && extras === undefined) scope.setExtras(extras)
scope.addAttachment({
filename: "extras-dump.json",
data: JSON.stringify(extras),
contentType: "application/json",
})
errs.forEach((err) => Sentry.captureException(err))
})
}
}
/**
* Reports a specific error to Sentry
* @param err The error to report
* @param tag The tag for the error
* @param extraTags Additional tag data to add
* @param extras Extra information to attach
*/
function reportError(
err: Error,
tag: SentryTag,
extraTags: Record<string, string | number | boolean> | null = null,
extras: any = undefined
) {
reportErrors([err], tag, extraTags, extras)
}
/**
* Subscribes to events occuring in various subsystems in the app
* for personalized error reporting
*/
function subscribeToAppEventsForReporting() {
gqlClientError$.subscribe((ev) => {
switch (ev.type) {
case "SUBSCRIPTION_CONN_CALLBACK_ERR_REPORT":
reportErrors(ev.errors, "BACKEND_OPERATIONS", { from: ev.type })
break
case "CLIENT_REPORTED_ERROR":
reportError(
ev.error,
"BACKEND_OPERATIONS",
{ from: ev.type },
{ op: ev.op }
)
break
case "GQL_CLIENT_REPORTED_ERROR":
reportError(
new Error("Backend Query Failed"),
"BACKEND_OPERATIONS",
{ opType: ev.opType },
{
opResult: ev.opResult,
}
)
break
}
})
}
/**
* Subscribe to app system events for adding
* additional data tags for the error reporting
*/
function subscribeForAppDataTags() {
const currentUser$ = platform.auth.getCurrentUserStream()
currentUser$.subscribe((user) => {
if (sentryActive) {
Sentry.setTag("user_logged_in", !!user)
}
})
}
export default <HoppModule>{
onRouterInit(app, router) {
if (!import.meta.env.VITE_SENTRY_DSN) {
console.log(
"Sentry tracing is not enabled because 'VITE_SENTRY_DSN' env is not defined"
)
return
}
if (settingsStore.value.TELEMETRY_ENABLED) {
initSentry(import.meta.env.VITE_SENTRY_DSN, router, app)
}
settingsStore.subject$.subscribe(({ TELEMETRY_ENABLED }) => {
if (!TELEMETRY_ENABLED && sentryActive) {
deinitSentry()
} else if (TELEMETRY_ENABLED && !sentryActive) {
initSentry(import.meta.env.VITE_SENTRY_DSN!, router, app)
}
})
subscribeToAppEventsForReporting()
subscribeForAppDataTags()
},
}

View File

@@ -60,6 +60,7 @@
<div class="space-y-4 py-4">
<div class="flex items-center">
<HoppSmartToggle
v-if="hasPlatformTelemetry"
:on="TELEMETRY_ENABLED"
@change="showConfirmModal"
>
@@ -134,6 +135,7 @@ import { InterceptorService } from "~/services/interceptor.service"
import { pipe } from "fp-ts/function"
import * as O from "fp-ts/Option"
import * as A from "fp-ts/Array"
import { platform } from "~/platform"
const t = useI18n()
const colorMode = useColorMode()
@@ -163,6 +165,8 @@ const TELEMETRY_ENABLED = useSetting("TELEMETRY_ENABLED")
const EXPAND_NAVIGATION = useSetting("EXPAND_NAVIGATION")
const SIDEBAR_ON_LEFT = useSetting("SIDEBAR_ON_LEFT")
const hasPlatformTelemetry = Boolean(platform.platformFeatureFlags.hasTelemetry)
const confirmRemove = ref(false)
const proxySettings = computed(() => ({

View File

@@ -26,6 +26,7 @@ export type PlatformDef = {
additionalInspectors?: InspectorsPlatformDef
platformFeatureFlags: {
exportAsGIST: boolean
hasTelemetry: boolean
}
}

View File

@@ -1,10 +1,10 @@
function generateREForProtocol(protocol) {
return [
new RegExp(
`${protocol}(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]).){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$`
`${protocol}(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]).){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])(:[0-9]+)?(\\/[^?#]*)?(\\?[^#]*)?(#.*)?$`
),
new RegExp(
`${protocol}(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]).)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9/])$`
`${protocol}(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9]).)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9/])(:[0-9]+)?(\\/[^?#]*)?(\\?[^#]*)?(#.*)?$`
),
]
}

View File

@@ -0,0 +1,92 @@
import fs from "fs"
import { glob } from "glob"
import path from "path"
import ts from "typescript"
import vueTsc from "vue-tsc"
import { fileURLToPath } from "url"
/**
* Helper function to find files to perform type check on
*/
const findFilesToPerformTypeCheck = (directoryPaths, filePatterns) => {
const files = []
directoryPaths.forEach((directoryPath) => {
if (!fs.existsSync(directoryPath)) {
console.error(`Directory not found: ${directoryPath}`)
process.exit(1)
}
files.push(
...glob.sync(filePatterns, {
cwd: directoryPath,
ignore: ["**/__tests__/**", "**/*.d.ts"],
absolute: true,
})
)
})
return files
}
// Derive the current file's directory path `__dirname` from the URL of this module `__filename`
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
// Define the directory paths and file patterns to perform type checks on
const directoryPaths = [path.resolve(__dirname, "src", "services")]
const filePatterns = ["**/*.ts"]
const tsConfigFileName = path.resolve(__dirname, "tsconfig.json")
const tsConfig = ts.readConfigFile(tsConfigFileName, ts.sys.readFile)
const { options } = ts.parseJsonConfigFileContent(
tsConfig.config,
ts.sys,
__dirname
)
const files = findFilesToPerformTypeCheck(directoryPaths, filePatterns)
const host = ts.createCompilerHost(options)
const program = vueTsc.createProgram({
rootNames: files,
options: { ...options, noEmit: true },
host,
})
// Perform type checking
const diagnostics = ts
.getPreEmitDiagnostics(program)
// Filter diagnostics to include only errors from files in the specified directory
.filter(({ file }) => {
if (!file) {
return false
}
return directoryPaths.some((directoryPath) =>
path.resolve(file.fileName).includes(directoryPath)
)
})
if (!diagnostics.length) {
console.log("Type checking passed.")
// Success
process.exit(0)
}
console.log("TypeScript diagnostics:")
const formatHost = {
getCanonicalFileName: (fileName) => fileName,
getCurrentDirectory: host.getCurrentDirectory,
getNewLine: () => ts.sys.newLine,
}
const formattedDiagnostics = ts.formatDiagnosticsWithColorAndContext(
diagnostics,
formatHost
)
console.error(formattedDiagnostics)
// Failure
process.exit(1)

View File

@@ -6,7 +6,9 @@
"main": "dist/hoppscotch-data.cjs",
"module": "dist/hoppscotch-data.js",
"types": "./dist/index.d.ts",
"files": [ "dist/*" ],
"files": [
"dist/*"
],
"scripts": {
"build:code": "vite build",
"build:decl": "tsc --project tsconfig.decl.json",
@@ -32,14 +34,16 @@
},
"homepage": "https://github.com/hoppscotch/hoppscotch#readme",
"devDependencies": {
"@types/lodash": "^4.14.181",
"typescript": "^4.6.3",
"vite": "^3.2.3"
"@types/lodash": "^4.14.200",
"typescript": "^5.2.2",
"vite": "^4.5.0"
},
"dependencies": {
"fp-ts": "^2.11.10",
"io-ts": "^2.2.16",
"fp-ts": "^2.16.1",
"io-ts": "^2.2.20",
"lodash": "^4.17.21",
"parser-ts": "^0.6.16"
"parser-ts": "^0.7.0",
"verzod": "^0.1.1",
"zod": "^3.22.4"
}
}

View File

@@ -1,14 +1,22 @@
import { pipe } from "fp-ts/function"
import * as E from "fp-ts/Either"
import { pipe } from "fp-ts/function"
import { InferredEntity, createVersionedEntity } from "verzod"
export type Environment = {
id?: string
name: string
variables: {
key: string
value: string
}[]
}
import V0_VERSION from "./v/0"
export const Environment = createVersionedEntity({
latestVersion: 0,
versionMap: {
0: V0_VERSION
},
getVersion(x) {
return V0_VERSION.schema.safeParse(x).success
? 0
: null
}
})
export type Environment = InferredEntity<typeof Environment>
const REGEX_ENV_VAR = /<<([^>]*)>>/g // "<<myVariable>>"

View File

@@ -0,0 +1,18 @@
import { z } from "zod"
import { defineVersion } from "verzod"
export const V0_SCHEMA = z.object({
id: z.optional(z.string()),
name: z.string(),
variables: z.array(
z.object({
key: z.string(),
value: z.string(),
})
)
})
export default defineVersion({
initial: true,
schema: V0_SCHEMA
})

View File

@@ -1,43 +0,0 @@
export type HoppGQLAuthNone = {
authType: "none"
}
export type HoppGQLAuthBasic = {
authType: "basic"
username: string
password: string
}
export type HoppGQLAuthBearer = {
authType: "bearer"
token: string
}
export type HoppGQLAuthOAuth2 = {
authType: "oauth-2"
token: string
oidcDiscoveryURL: string
authURL: string
accessTokenURL: string
clientID: string
scope: string
}
export type HoppGQLAuthAPIKey = {
authType: "api-key"
key: string
value: string
addTo: string
}
export type HoppGQLAuth = { authActive: boolean } & (
| HoppGQLAuthNone
| HoppGQLAuthBasic
| HoppGQLAuthBearer
| HoppGQLAuthOAuth2
| HoppGQLAuthAPIKey
)

View File

@@ -1,51 +1,75 @@
import { HoppGQLAuth } from "./HoppGQLAuth"
import { InferredEntity, createVersionedEntity } from "verzod"
import { z } from "zod"
import V1_VERSION from "./v/1"
import V2_VERSION from "./v/2"
export * from "./HoppGQLAuth"
export { GQLHeader } from "./v/1"
export {
HoppGQLAuth,
HoppGQLAuthAPIKey,
HoppGQLAuthBasic,
HoppGQLAuthBearer,
HoppGQLAuthNone,
HoppGQLAuthOAuth2,
} from "./v/2"
export const GQL_REQ_SCHEMA_VERSION = 2
export type GQLHeader = {
key: string
value: string
active: boolean
}
const versionedObject = z.object({
v: z.number(),
})
export type HoppGQLRequest = {
id?: string
v: number
name: string
url: string
headers: GQLHeader[]
query: string
variables: string
auth: HoppGQLAuth
}
export const HoppGQLRequest = createVersionedEntity({
latestVersion: 2,
versionMap: {
1: V1_VERSION,
2: V2_VERSION,
},
getVersion(x) {
const result = versionedObject.safeParse(x)
export function translateToGQLRequest(x: any): HoppGQLRequest {
if (x.v && x.v === GQL_REQ_SCHEMA_VERSION) return x
return result.success ? result.data.v : null
},
})
// Old request
const name = x.name ?? "Untitled"
const url = x.url ?? ""
const headers = x.headers ?? []
const query = x.query ?? ""
const variables = x.variables ?? []
const auth = x.auth ?? {
authType: "none",
authActive: true,
export type HoppGQLRequest = InferredEntity<typeof HoppGQLRequest>
const DEFAULT_QUERY = `
query Request {
method
url
headers {
key
value
}
}`.trim()
export function getDefaultGQLRequest(): HoppGQLRequest {
return {
v: GQL_REQ_SCHEMA_VERSION,
name,
url,
headers,
query,
variables,
auth
name: "Untitled",
url: "https://echo.hoppscotch.io/graphql",
headers: [],
variables: `
{
"id": "1"
}`.trim(),
query: DEFAULT_QUERY,
auth: {
authType: "none",
authActive: true,
},
}
}
/**
* @deprecated This function is deprecated. Use `HoppGQLRequest` instead.
*/
export function translateToGQLRequest(x: unknown): HoppGQLRequest {
const result = HoppGQLRequest.safeParse(x)
return result.type === "ok" ? result.value : getDefaultGQLRequest()
}
export function makeGQLRequest(x: Omit<HoppGQLRequest, "v">): HoppGQLRequest {
return {
v: GQL_REQ_SCHEMA_VERSION,

View File

@@ -0,0 +1,24 @@
import { z } from "zod"
import { defineVersion } from "verzod"
export const GQLHeader = z.object({
key: z.string(),
value: z.string(),
active: z.boolean()
})
export type GQLHeader = z.infer<typeof GQLHeader>
export const V1_SCHEMA = z.object({
v: z.literal(1),
name: z.string(),
url: z.string(),
headers: z.array(GQLHeader),
query: z.string(),
variables: z.string(),
})
export default defineVersion({
initial: true,
schema: V1_SCHEMA
})

View File

@@ -0,0 +1,91 @@
import { z } from "zod"
import { defineVersion } from "verzod"
import { GQLHeader, V1_SCHEMA } from "./1"
export const HoppGQLAuthNone = z.object({
authType: z.literal("none")
})
export type HoppGQLAuthNone = z.infer<typeof HoppGQLAuthNone>
export const HoppGQLAuthBasic = z.object({
authType: z.literal("basic"),
username: z.string(),
password: z.string()
})
export type HoppGQLAuthBasic = z.infer<typeof HoppGQLAuthBasic>
export const HoppGQLAuthBearer = z.object({
authType: z.literal("bearer"),
token: z.string()
})
export type HoppGQLAuthBearer = z.infer<typeof HoppGQLAuthBearer>
export const HoppGQLAuthOAuth2 = z.object({
authType: z.literal("oauth-2"),
token: z.string(),
oidcDiscoveryURL: z.string(),
authURL: z.string(),
accessTokenURL: z.string(),
clientID: z.string(),
scope: z.string()
})
export type HoppGQLAuthOAuth2 = z.infer<typeof HoppGQLAuthOAuth2>
export const HoppGQLAuthAPIKey = z.object({
authType: z.literal("api-key"),
key: z.string(),
value: z.string(),
addTo: z.string()
})
export type HoppGQLAuthAPIKey = z.infer<typeof HoppGQLAuthAPIKey>
export const HoppGQLAuth = z.discriminatedUnion("authType", [
HoppGQLAuthNone,
HoppGQLAuthBasic,
HoppGQLAuthBearer,
HoppGQLAuthOAuth2,
HoppGQLAuthAPIKey
]).and(
z.object({
authActive: z.boolean()
})
)
export type HoppGQLAuth = z.infer<typeof HoppGQLAuth>
const V2_SCHEMA = z.object({
id: z.optional(z.string()),
v: z.literal(2),
name: z.string(),
url: z.string(),
headers: z.array(GQLHeader),
query: z.string(),
variables: z.string(),
auth: HoppGQLAuth
})
export default defineVersion({
initial: false,
schema: V2_SCHEMA,
up(old: z.infer<typeof V1_SCHEMA>) {
return <z.infer<typeof V2_SCHEMA>>{
...old,
v: 2,
auth: {
authActive: true,
authType: "none",
}
}
}
})

View File

@@ -1,43 +0,0 @@
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 HoppRESTAuthAPIKey = {
authType: "api-key"
key: string
value: string
addTo: string
}
export type HoppRESTAuth = { authActive: boolean } & (
| HoppRESTAuthNone
| HoppRESTAuthBasic
| HoppRESTAuthBearer
| HoppRESTAuthOAuth2
| HoppRESTAuthAPIKey
)

View File

@@ -11,3 +11,5 @@ export const knownContentTypes = {
}
export type ValidContentTypes = keyof typeof knownContentTypes
export const ValidContentTypesList = Object.keys(knownContentTypes) as ValidContentTypes[]

View File

@@ -1,66 +1,58 @@
import cloneDeep from "lodash/cloneDeep"
import * as Eq from "fp-ts/Eq"
import * as S from "fp-ts/string"
import { ValidContentTypes } from "./content-types"
import { HoppRESTAuth } from "./HoppRESTAuth"
import cloneDeep from "lodash/cloneDeep"
import V0_VERSION from "./v/0"
import V1_VERSION from "./v/1"
import { createVersionedEntity, InferredEntity } from "verzod"
import { lodashIsEqualEq, mapThenEq, undefinedEq } from "../utils/eq"
import {
HoppRESTAuth,
HoppRESTReqBody,
HoppRESTHeaders,
HoppRESTParams,
} from "./v/1"
import { z } from "zod"
export * from "./content-types"
export * from "./HoppRESTAuth"
export {
FormDataKeyValue,
HoppRESTReqBodyFormData,
HoppRESTAuth,
HoppRESTAuthAPIKey,
HoppRESTAuthBasic,
HoppRESTAuthBearer,
HoppRESTAuthNone,
HoppRESTAuthOAuth2,
HoppRESTReqBody,
} from "./v/1"
export const RESTReqSchemaVersion = "1"
const versionedObject = z.object({
// v is a stringified number
v: z.string().regex(/^\d+$/).transform(Number),
})
export type HoppRESTParam = {
key: string
value: string
active: boolean
}
export const HoppRESTRequest = createVersionedEntity({
latestVersion: 1,
versionMap: {
0: V0_VERSION,
1: V1_VERSION,
},
getVersion(data) {
// For V1 onwards we have the v string storing the number
const versionCheck = versionedObject.safeParse(data)
export type HoppRESTHeader = {
key: string
value: string
active: boolean
}
if (versionCheck.success) return versionCheck.data.v
export type FormDataKeyValue = {
key: string
active: boolean
} & ({ isFile: true; value: Blob[] } | { isFile: false; value: string })
// For V0 we have to check the schema
const result = V0_VERSION.schema.safeParse(data)
export type HoppRESTReqBodyFormData = {
contentType: "multipart/form-data"
body: FormDataKeyValue[]
}
return result.success ? 0 : null
},
})
export type HoppRESTReqBody =
| {
contentType: Exclude<ValidContentTypes, "multipart/form-data">
body: string
}
| HoppRESTReqBodyFormData
| {
contentType: null
body: null
}
export type HoppRESTRequest = InferredEntity<typeof HoppRESTRequest>
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 const HoppRESTRequestEq = Eq.struct<HoppRESTRequest>({
const HoppRESTRequestEq = Eq.struct<HoppRESTRequest>({
id: undefinedEq(S.Eq),
v: S.Eq,
auth: lodashIsEqualEq,
@@ -80,6 +72,11 @@ export const HoppRESTRequestEq = Eq.struct<HoppRESTRequest>({
testScript: S.Eq,
})
export const RESTReqSchemaVersion = "1"
export type HoppRESTParam = HoppRESTRequest["params"][number]
export type HoppRESTHeader = HoppRESTRequest["headers"][number]
export const isEqualHoppRESTRequest = HoppRESTRequestEq.equals
/**
@@ -87,6 +84,9 @@ export const isEqualHoppRESTRequest = HoppRESTRequestEq.equals
* If we fail to detect certain bits, we just resolve it to the default value
* @param x The value to extract REST Request data from
* @param defaultReq The default REST Request to source from
*
* @deprecated Usage of this function is no longer recommended and is only here
* for legacy reasons and will be removed
*/
export function safelyExtractRESTRequest(
x: unknown,
@@ -94,40 +94,53 @@ export function safelyExtractRESTRequest(
): HoppRESTRequest {
const req = cloneDeep(defaultReq)
// TODO: A cleaner way to do this ?
if (!!x && typeof x === "object") {
if (x.hasOwnProperty("v") && typeof x.v === "string")
req.v = x.v
if ("id" in x && typeof x.id === "string") req.id = x.id
if (x.hasOwnProperty("id") && typeof x.id === "string")
req.id = x.id
if ("name" in x && typeof x.name === "string") req.name = x.name
if (x.hasOwnProperty("name") && typeof x.name === "string")
req.name = x.name
if ("method" in x && typeof x.method === "string") req.method = x.method
if (x.hasOwnProperty("method") && typeof x.method === "string")
req.method = x.method
if (x.hasOwnProperty("endpoint") && typeof x.endpoint === "string")
if ("endpoint" in x && typeof x.endpoint === "string")
req.endpoint = x.endpoint
if (x.hasOwnProperty("preRequestScript") && typeof x.preRequestScript === "string")
if ("preRequestScript" in x && typeof x.preRequestScript === "string")
req.preRequestScript = x.preRequestScript
if (x.hasOwnProperty("testScript") && typeof x.testScript === "string")
if ("testScript" in x && typeof x.testScript === "string")
req.testScript = x.testScript
if (x.hasOwnProperty("body") && typeof x.body === "object" && !!x.body)
req.body = x.body as any // TODO: Deep nested checks
if ("body" in x) {
const result = HoppRESTReqBody.safeParse(x.body)
if (x.hasOwnProperty("auth") && typeof x.auth === "object" && !!x.auth)
req.auth = x.auth as any // TODO: Deep nested checks
if (result.success) {
req.body = result.data
}
}
if (x.hasOwnProperty("params") && Array.isArray(x.params))
req.params = x.params // TODO: Deep nested checks
if ("auth" in x) {
const result = HoppRESTAuth.safeParse(x.auth)
if (x.hasOwnProperty("headers") && Array.isArray(x.headers))
req.headers = x.headers // TODO: Deep nested checks
if (result.success) {
req.auth = result.data
}
}
if ("params" in x) {
const result = HoppRESTParams.safeParse(x.params)
if (result.success) {
req.params = result.data
}
}
if ("headers" in x) {
const result = HoppRESTHeaders.safeParse(x.headers)
if (result.success) {
req.headers = result.data
}
}
}
return req
@@ -137,105 +150,51 @@ export function makeRESTRequest(
x: Omit<HoppRESTRequest, "v">
): HoppRESTRequest {
return {
...x,
v: RESTReqSchemaVersion,
...x,
}
}
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,
}
}
export function getDefaultRESTRequest(): HoppRESTRequest {
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 ?? "Untitled request"
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 {
v: "1",
endpoint: "https://echo.hoppscotch.io",
name: "Untitled",
params: [],
headers: [],
method: "GET",
auth: {
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 }
},
preRequestScript: "",
testScript: "",
body: {
contentType: null,
body: null,
},
}
}
/**
* Checks if the given value is a HoppRESTRequest
* @param x The value to check
*
* @deprecated This function is no longer recommended and is only here for legacy reasons
* Use `HoppRESTRequest.is`/`HoppRESTRequest.isLatest` instead.
*/
export function isHoppRESTRequest(x: unknown): x is HoppRESTRequest {
return HoppRESTRequest.isLatest(x)
}
/**
* Safely parses a value into a HoppRESTRequest.
* @param x The value to check
*
* @deprecated This function is no longer recommended and is only here for
* legacy reasons. Use `HoppRESTRequest.safeParse` instead.
*/
export function translateToNewRequest(x: unknown): HoppRESTRequest {
const result = HoppRESTRequest.safeParse(x)
return result.type === "ok" ? result.value : getDefaultRESTRequest()
}

View File

@@ -0,0 +1,39 @@
import { defineVersion } from "verzod"
import { z } from "zod"
export const V0_SCHEMA = z.object({
id: z.optional(z.string()), // Firebase Firestore ID
url: z.string(),
path: z.string(),
headers: z.array(
z.object({
key: z.string(),
value: z.string(),
active: z.boolean()
})
),
params: z.array(
z.object({
key: z.string(),
value: z.string(),
active: z.boolean()
})
),
name: z.string(),
method: z.string(),
preRequestScript: z.string(),
testScript: z.string(),
contentType: z.string(),
body: z.string(),
rawParams: z.optional(z.string()),
auth: z.optional(z.string()),
httpUser: z.optional(z.string()),
httpPassword: z.optional(z.string()),
bearerToken: z.optional(z.string()),
})
export default defineVersion({
initial: true,
schema: V0_SCHEMA
})

View File

@@ -0,0 +1,209 @@
import { defineVersion } from "verzod"
import { z } from "zod"
import { V0_SCHEMA } from "./0"
export const FormDataKeyValue = z.object({
key: z.string(),
active: z.boolean()
}).and(
z.union([
z.object({
isFile: z.literal(true),
value: z.array(z.instanceof(Blob))
}),
z.object({
isFile: z.literal(false),
value: z.string()
})
])
)
export type FormDataKeyValue = z.infer<typeof FormDataKeyValue>
export const HoppRESTReqBodyFormData = z.object({
contentType: z.literal("multipart/form-data"),
body: z.array(FormDataKeyValue)
})
export type HoppRESTReqBodyFormData = z.infer<typeof HoppRESTReqBodyFormData>
export const HoppRESTReqBody = z.union([
z.object({
contentType: z.literal(null),
body: z.literal(null)
}),
z.object({
contentType: z.literal("multipart/form-data"),
body: FormDataKeyValue
}),
z.object({
contentType: z.union([
z.literal("application/json"),
z.literal("application/ld+json"),
z.literal("application/hal+json"),
z.literal("application/vnd.api+json"),
z.literal("application/xml"),
z.literal("application/x-www-form-urlencoded"),
z.literal("text/html"),
z.literal("text/plain"),
]),
body: z.string()
})
])
export type HoppRESTReqBody = z.infer<typeof HoppRESTReqBody>
export const HoppRESTAuthNone = z.object({
authType: z.literal("none")
})
export type HoppRESTAuthNone = z.infer<typeof HoppRESTAuthNone>
export const HoppRESTAuthBasic = z.object({
authType: z.literal("basic"),
username: z.string(),
password: z.string(),
})
export type HoppRESTAuthBasic = z.infer<typeof HoppRESTAuthBasic>
export const HoppRESTAuthBearer = z.object({
authType: z.literal("bearer"),
token: z.string(),
})
export type HoppRESTAuthBearer = z.infer<typeof HoppRESTAuthBearer>
export const HoppRESTAuthOAuth2 = z.object({
authType: z.literal("oauth-2"),
token: z.string(),
oidcDiscoveryURL: z.string(),
authURL: z.string(),
accessTokenURL: z.string(),
clientID: z.string(),
scope: z.string(),
})
export type HoppRESTAuthOAuth2 = z.infer<typeof HoppRESTAuthOAuth2>
export const HoppRESTAuthAPIKey = z.object({
authType: z.literal("api-key"),
key: z.string(),
value: z.string(),
addTo: z.string(),
})
export type HoppRESTAuthAPIKey = z.infer<typeof HoppRESTAuthAPIKey>
export const HoppRESTAuth = z.discriminatedUnion("authType", [
HoppRESTAuthNone,
HoppRESTAuthBasic,
HoppRESTAuthBearer,
HoppRESTAuthOAuth2,
HoppRESTAuthAPIKey
]).and(
z.object({
authActive: z.boolean(),
})
)
export type HoppRESTAuth = z.infer<typeof HoppRESTAuth>
export const HoppRESTParams = z.array(
z.object({
key: z.string(),
value: z.string(),
active: z.boolean()
})
)
export type HoppRESTParams = z.infer<typeof HoppRESTParams>
export const HoppRESTHeaders = z.array(
z.object({
key: z.string(),
value: z.string(),
active: z.boolean()
})
)
export type HoppRESTHeaders = z.infer<typeof HoppRESTHeaders>
const V1_SCHEMA = z.object({
v: z.literal("1"),
id: z.optional(z.string()), // Firebase Firestore ID
name: z.string(),
method: z.string(),
endpoint: z.string(),
params: HoppRESTParams,
headers: HoppRESTHeaders,
preRequestScript: z.string(),
testScript: z.string(),
auth: HoppRESTAuth,
body: HoppRESTReqBody
})
function parseRequestBody(x: z.infer<typeof V0_SCHEMA>): z.infer<typeof V1_SCHEMA>["body"] {
return {
contentType: "application/json",
body: x.contentType === "application/json" ? x.rawParams ?? "" : "",
}
}
export function parseOldAuth(x: z.infer<typeof V0_SCHEMA>): z.infer<typeof V1_SCHEMA>["auth"] {
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 }
}
export default defineVersion({
initial: false,
schema: V1_SCHEMA,
up(old: z.infer<typeof V0_SCHEMA>) {
const { url, path, headers, params, name, method, preRequestScript, testScript } = old
const endpoint = `${url}${path}`
const body = parseRequestBody(old)
const auth = parseOldAuth(old)
const result: z.infer<typeof V1_SCHEMA> = {
v: "1",
endpoint,
headers,
params,
name,
method,
preRequestScript,
testScript,
body,
auth,
}
if (old.id) result.id = old.id
return result
},
})

View File

@@ -2,7 +2,7 @@
"compilerOptions": {
"target": "es2017",
"module": "esnext",
"lib": ["esnext"],
"lib": ["esnext", "DOM"],
"moduleResolution": "node",
"esModuleInterop": true,
"strict": true,

View File

@@ -2,13 +2,13 @@
"compilerOptions": {
"target": "es2017",
"module": "esnext",
"lib": ["esnext"],
"lib": ["esnext", "DOM"],
"moduleResolution": "node",
"esModuleInterop": true,
"strict": true,
"strictNullChecks": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"resolveJsonModule": true
},
"include": ["src/*.ts"]
}

View File

@@ -1,6 +1,6 @@
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
testEnvironment: "jsdom",
collectCoverage: true,
setupFilesAfterEnv: ["./jest.setup.ts"],
}

View File

@@ -38,5 +38,6 @@ createHoppApp("#app", {
],
platformFeatureFlags: {
exportAsGIST: false,
hasTelemetry: false,
},
})

View File

@@ -151,6 +151,7 @@ export default defineConfig({
},
}),
VitePWA({
useCredentials: true,
manifest: {
name: APP_INFO.name,
short_name: APP_INFO.name,

View File

@@ -21,7 +21,6 @@
"@fontsource-variable/material-symbols-rounded": "^5.0.5",
"@fontsource-variable/roboto-mono": "^5.0.6",
"@hoppscotch/vue-toasted": "^0.1.0",
"@lezer/highlight": "^1.0.0",
"@vitejs/plugin-legacy": "^2.3.0",
"@vueuse/core": "^8.7.5",
"fp-ts": "^2.12.1",
@@ -97,4 +96,4 @@
"./helpers/treeAdapter.ts": "./src/helpers/treeAdapter.ts"
},
"types": "./dist/index.d.ts"
}
}