refactor: move persistence logic into a dedicated service (#3493)

This commit is contained in:
James George
2023-11-29 22:40:26 +05:30
committed by GitHub
parent 144d14ab5b
commit 60bfb6fe2c
18 changed files with 3179 additions and 647 deletions

View File

@@ -0,0 +1,208 @@
import {
Environment,
HoppCollection,
HoppGQLRequest,
HoppRESTRequest,
} from "@hoppscotch/data"
import { HoppGQLDocument } from "~/helpers/graphql/document"
import { HoppRESTDocument } from "~/helpers/rest/document"
import { GQLHistoryEntry, RESTHistoryEntry } from "~/newstore/history"
import { SettingsDef, getDefaultSettings } from "~/newstore/settings"
import { PersistableTabState } from "~/services/tab"
type VUEX_DATA = {
postwoman: {
settings?: SettingsDef
collections?: HoppCollection<HoppRESTRequest>[]
collectionsGraphql?: HoppCollection<HoppGQLRequest>[]
environments?: Environment[]
}
}
const DEFAULT_SETTINGS = getDefaultSettings()
export const REST_COLLECTIONS_MOCK: HoppCollection<HoppRESTRequest>[] = [
{
v: 1,
name: "Echo",
folders: [],
requests: [
{
v: "1",
endpoint: "https://echo.hoppscotch.io",
name: "Echo test",
params: [],
headers: [],
method: "GET",
auth: { authType: "none", authActive: true },
preRequestScript: "",
testScript: "",
body: { contentType: null, body: null },
},
],
},
]
export const GQL_COLLECTIONS_MOCK: HoppCollection<HoppGQLRequest>[] = [
{
v: 1,
name: "Echo",
folders: [],
requests: [
{
v: 2,
name: "Echo test",
url: "https://echo.hoppscotch.io/graphql",
headers: [],
variables: '{\n "id": "1"\n}',
query: "query Request { url }",
auth: { authType: "none", authActive: true },
},
],
},
]
export const ENVIRONMENTS_MOCK: Environment[] = [
{
name: "globals",
variables: [
{
key: "test-global-key",
value: "test-global-value",
},
],
},
{ name: "Test", variables: [{ key: "test-key", value: "test-value" }] },
]
export const SELECTED_ENV_INDEX_MOCK = {
type: "MY_ENV",
index: 1,
}
export const WEBSOCKET_REQUEST_MOCK = {
endpoint: "wss://echo-websocket.hoppscotch.io",
protocols: [],
}
export const SOCKET_IO_REQUEST_MOCK = {
endpoint: "wss://echo-socketio.hoppscotch.io",
path: "/socket.io",
version: "v4",
}
export const SSE_REQUEST_MOCK = {
endpoint: "https://express-eventsource.herokuapp.com/events",
eventType: "data",
}
export const MQTT_REQUEST_MOCK = {
endpoint: "wss://test.mosquitto.org:8081",
clientID: "hoppscotch",
}
export const GLOBAL_ENV_MOCK: Environment["variables"] = [
{ key: "test-key", value: "test-value" },
]
export const VUEX_DATA_MOCK: VUEX_DATA = {
postwoman: {
settings: { ...DEFAULT_SETTINGS, THEME_COLOR: "purple" },
collections: REST_COLLECTIONS_MOCK,
collectionsGraphql: GQL_COLLECTIONS_MOCK,
environments: ENVIRONMENTS_MOCK,
},
}
export const REST_HISTORY_MOCK: RESTHistoryEntry[] = [
{
v: 1,
request: {
auth: { authType: "none", authActive: true },
body: { contentType: null, body: null },
endpoint: "https://echo.hoppscotch.io",
headers: [],
method: "GET",
name: "Untitled",
params: [],
preRequestScript: "",
testScript: "",
v: "1",
},
responseMeta: { duration: 807, statusCode: 200 },
star: false,
updatedOn: new Date("2023-11-07T05:27:32.951Z"),
},
]
export const GQL_HISTORY_MOCK: GQLHistoryEntry[] = [
{
v: 1,
request: {
v: 2,
name: "Untitled",
url: "https://echo.hoppscotch.io/graphql",
query: "query Request { url }",
headers: [],
variables: "",
auth: { authType: "none", authActive: true },
},
response: '{"data":{"url":"/graphql"}}',
star: false,
updatedOn: new Date("2023-11-07T05:28:21.073Z"),
},
]
export const GQL_TAB_STATE_MOCK: PersistableTabState<HoppGQLDocument> = {
lastActiveTabID: "5edbe8d4-65c9-4381-9354-5f1bf05d8ccc",
orderedDocs: [
{
tabID: "5edbe8d4-65c9-4381-9354-5f1bf05d8ccc",
doc: {
request: {
v: 2,
name: "Untitled",
url: "https://echo.hoppscotch.io/graphql",
headers: [],
variables: '{\n "id": "1"\n}',
query: "query Request { url }",
auth: { authType: "none", authActive: true },
},
isDirty: true,
optionTabPreference: "query",
response: null,
},
},
],
}
export const REST_TAB_STATE_MOCK: PersistableTabState<HoppRESTDocument> = {
lastActiveTabID: "e6e8d800-caa8-44a2-a6a6-b4765a3167aa",
orderedDocs: [
{
tabID: "e6e8d800-caa8-44a2-a6a6-b4765a3167aa",
doc: {
request: {
v: "1",
endpoint: "https://echo.hoppscotch.io",
name: "Echo test",
params: [],
headers: [],
method: "GET",
auth: { authType: "none", authActive: true },
preRequestScript: "",
testScript: "",
body: { contentType: null, body: null },
},
isDirty: false,
saveContext: {
originLocation: "user-collection",
folderPath: "0",
requestIndex: 0,
},
response: null,
},
},
],
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,724 @@
/* eslint-disable no-restricted-globals, no-restricted-syntax */
import {
Environment,
translateToNewGQLCollection,
translateToNewRESTCollection,
} from "@hoppscotch/data"
import { StorageLike, watchDebounced } from "@vueuse/core"
import { Service } from "dioc"
import { assign, clone, isEmpty } from "lodash-es"
import { z } from "zod"
import { GQLTabService } from "~/services/tab/graphql"
import { RESTTabService } from "~/services/tab/rest"
import { useToast } from "~/composables/toast"
import { MQTTRequest$, setMQTTRequest } from "../../newstore/MQTTSession"
import { SSERequest$, setSSERequest } from "../../newstore/SSESession"
import { SIORequest$, setSIORequest } from "../../newstore/SocketIOSession"
import { WSRequest$, setWSRequest } from "../../newstore/WebSocketSession"
import {
graphqlCollectionStore,
restCollectionStore,
setGraphqlCollections,
setRESTCollections,
} from "../../newstore/collections"
import {
addGlobalEnvVariable,
environments$,
globalEnv$,
replaceEnvironments,
selectedEnvironmentIndex$,
setGlobalEnvVariables,
setSelectedEnvironmentIndex,
} from "../../newstore/environments"
import {
graphqlHistoryStore,
restHistoryStore,
setGraphqlHistoryEntries,
setRESTHistoryEntries,
translateToNewGQLHistory,
translateToNewRESTHistory,
} from "../../newstore/history"
import { bulkApplyLocalState, localStateStore } from "../../newstore/localstate"
import {
HoppAccentColor,
HoppBgColor,
applySetting,
bulkApplySettings,
getDefaultSettings,
performSettingsDataMigrations,
settingsStore,
} from "../../newstore/settings"
import {
ENVIRONMENTS_SCHEMA,
GLOBAL_ENV_SCHEMA,
GQL_COLLECTION_SCHEMA,
GQL_HISTORY_ENTRY_SCHEMA,
GQL_TAB_STATE_SCHEMA,
LOCAL_STATE_SCHEMA,
MQTT_REQUEST_SCHEMA,
NUXT_COLOR_MODE_SCHEMA,
REST_COLLECTION_SCHEMA,
REST_HISTORY_ENTRY_SCHEMA,
REST_TAB_STATE_SCHEMA,
SELECTED_ENV_INDEX_SCHEMA,
SETTINGS_SCHEMA,
SOCKET_IO_REQUEST_SCHEMA,
SSE_REQUEST_SCHEMA,
THEME_COLOR_SCHEMA,
VUEX_SCHEMA,
WEBSOCKET_REQUEST_SCHEMA,
} from "./validation-schemas"
/**
* This service compiles persistence logic across the codebase
*/
export class PersistenceService extends Service {
public static readonly ID = "PERSISTENCE_SERVICE"
private readonly restTabService = this.bind(RESTTabService)
private readonly gqlTabService = this.bind(GQLTabService)
public hoppLocalConfigStorage: StorageLike = localStorage
constructor() {
super()
}
private showErrorToast(localStorageKey: string) {
const toast = useToast()
toast.error(
`There's a mismatch with the expected schema for the value corresponding to ${localStorageKey} read from localStorage, keeping a backup in ${localStorageKey}-backup`
)
}
private checkAndMigrateOldSettings() {
if (window.localStorage.getItem("selectedEnvIndex")) {
const index = window.localStorage.getItem("selectedEnvIndex")
if (index) {
if (index === "-1") {
window.localStorage.setItem(
"selectedEnvIndex",
JSON.stringify({
type: "NO_ENV_SELECTED",
})
)
} else if (Number(index) >= 0) {
window.localStorage.setItem(
"selectedEnvIndex",
JSON.stringify({
type: "MY_ENV",
index: parseInt(index),
})
)
}
}
}
const vuexKey = "vuex"
let vuexData = JSON.parse(window.localStorage.getItem(vuexKey) || "{}")
if (isEmpty(vuexData)) return
// Validate data read from localStorage
const result = VUEX_SCHEMA.safeParse(vuexData)
if (result.success) {
vuexData = result.data
} else {
this.showErrorToast(vuexKey)
window.localStorage.setItem(`${vuexKey}-backup`, JSON.stringify(vuexData))
}
const { postwoman } = vuexData
if (!isEmpty(postwoman?.settings)) {
const settingsData = assign(
clone(getDefaultSettings()),
postwoman.settings
)
window.localStorage.setItem("settings", JSON.stringify(settingsData))
delete postwoman.settings
window.localStorage.setItem(vuexKey, JSON.stringify(vuexData))
}
if (postwoman?.collections) {
window.localStorage.setItem(
"collections",
JSON.stringify(postwoman.collections)
)
delete postwoman.collections
window.localStorage.setItem(vuexKey, JSON.stringify(vuexData))
}
if (postwoman?.collectionsGraphql) {
window.localStorage.setItem(
"collectionsGraphql",
JSON.stringify(postwoman.collectionsGraphql)
)
delete postwoman.collectionsGraphql
window.localStorage.setItem(vuexKey, JSON.stringify(vuexData))
}
if (postwoman?.environments) {
window.localStorage.setItem(
"environments",
JSON.stringify(postwoman.environments)
)
delete postwoman.environments
window.localStorage.setItem(vuexKey, JSON.stringify(vuexData))
}
const themeColorKey = "THEME_COLOR"
let themeColorValue = window.localStorage.getItem(themeColorKey)
if (themeColorValue) {
// Validate data read from localStorage
const result = THEME_COLOR_SCHEMA.safeParse(themeColorValue)
if (result.success) {
themeColorValue = result.data
} else {
this.showErrorToast(themeColorKey)
window.localStorage.setItem(`${themeColorKey}-backup`, themeColorValue)
}
applySetting(themeColorKey, themeColorValue as HoppAccentColor)
window.localStorage.removeItem(themeColorKey)
}
const nuxtColorModeKey = "nuxt-color-mode"
let nuxtColorModeValue = window.localStorage.getItem(nuxtColorModeKey)
if (nuxtColorModeValue) {
// Validate data read from localStorage
const result = NUXT_COLOR_MODE_SCHEMA.safeParse(nuxtColorModeValue)
if (result.success) {
nuxtColorModeValue = result.data
} else {
this.showErrorToast(nuxtColorModeKey)
window.localStorage.setItem(
`${nuxtColorModeKey}-backup`,
nuxtColorModeValue
)
}
applySetting("BG_COLOR", nuxtColorModeValue as HoppBgColor)
window.localStorage.removeItem(nuxtColorModeKey)
}
}
public setupLocalStatePersistence() {
const localStateKey = "localState"
let localStateData = JSON.parse(
window.localStorage.getItem(localStateKey) ?? "{}"
)
// Validate data read from localStorage
const result = LOCAL_STATE_SCHEMA.safeParse(localStateData)
if (result.success) {
localStateData = result.data
} else {
this.showErrorToast(localStateKey)
window.localStorage.setItem(
`${localStateKey}-backup`,
JSON.stringify(localStateData)
)
}
if (localStateData) bulkApplyLocalState(localStateData)
localStateStore.subject$.subscribe((state) => {
window.localStorage.setItem(localStateKey, JSON.stringify(state))
})
}
private setupSettingsPersistence() {
const settingsKey = "settings"
let settingsData = JSON.parse(
window.localStorage.getItem(settingsKey) || "{}"
)
// Validate data read from localStorage
const result = SETTINGS_SCHEMA.safeParse(settingsData)
if (result.success) {
settingsData = result.data
} else {
this.showErrorToast(settingsKey)
window.localStorage.setItem(
`${settingsKey}-backup`,
JSON.stringify(settingsData)
)
}
const updatedSettings = settingsData
? performSettingsDataMigrations(settingsData)
: settingsData
if (updatedSettings) {
bulkApplySettings(updatedSettings)
}
settingsStore.subject$.subscribe((settings) => {
window.localStorage.setItem(settingsKey, JSON.stringify(settings))
})
}
private setupHistoryPersistence() {
const restHistoryKey = "history"
let restHistoryData = JSON.parse(
window.localStorage.getItem(restHistoryKey) || "[]"
)
const graphqlHistoryKey = "graphqlHistory"
let graphqlHistoryData = JSON.parse(
window.localStorage.getItem(graphqlHistoryKey) || "[]"
)
// Validate data read from localStorage
const restHistorySchemaParsedresult = z
.array(REST_HISTORY_ENTRY_SCHEMA)
.safeParse(restHistoryData)
if (restHistorySchemaParsedresult.success) {
restHistoryData = restHistorySchemaParsedresult.data
} else {
this.showErrorToast(restHistoryKey)
window.localStorage.setItem(
`${restHistoryKey}-backup`,
JSON.stringify(restHistoryData)
)
}
const gqlHistorySchemaParsedresult = z
.array(GQL_HISTORY_ENTRY_SCHEMA)
.safeParse(graphqlHistoryData)
if (gqlHistorySchemaParsedresult.success) {
graphqlHistoryData = gqlHistorySchemaParsedresult.data
} else {
this.showErrorToast(graphqlHistoryKey)
window.localStorage.setItem(
`${graphqlHistoryKey}-backup`,
JSON.stringify(graphqlHistoryData)
)
}
const translatedRestHistoryData = restHistoryData.map(
translateToNewRESTHistory
)
const translatedGraphqlHistoryData = graphqlHistoryData.map(
translateToNewGQLHistory
)
setRESTHistoryEntries(translatedRestHistoryData)
setGraphqlHistoryEntries(translatedGraphqlHistoryData)
restHistoryStore.subject$.subscribe(({ state }) => {
window.localStorage.setItem(restHistoryKey, JSON.stringify(state))
})
graphqlHistoryStore.subject$.subscribe(({ state }) => {
window.localStorage.setItem(graphqlHistoryKey, JSON.stringify(state))
})
}
private setupCollectionsPersistence() {
const restCollectionsKey = "collections"
let restCollectionsData = JSON.parse(
window.localStorage.getItem(restCollectionsKey) || "[]"
)
const graphqlCollectionsKey = "collectionsGraphql"
let graphqlCollectionsData = JSON.parse(
window.localStorage.getItem(graphqlCollectionsKey) || "[]"
)
// Validate data read from localStorage
const restCollectionsSchemaParsedresult = z
.array(REST_COLLECTION_SCHEMA)
.safeParse(restCollectionsData)
if (restCollectionsSchemaParsedresult.success) {
restCollectionsData = restCollectionsSchemaParsedresult.data
} else {
this.showErrorToast(restCollectionsKey)
window.localStorage.setItem(
`${restCollectionsKey}-backup`,
JSON.stringify(restCollectionsData)
)
}
const gqlCollectionsSchemaParsedresult = z
.array(GQL_COLLECTION_SCHEMA)
.safeParse(graphqlCollectionsData)
if (gqlCollectionsSchemaParsedresult.success) {
graphqlCollectionsData = gqlCollectionsSchemaParsedresult.data
} else {
this.showErrorToast(graphqlCollectionsKey)
window.localStorage.setItem(
`${graphqlCollectionsKey}-backup`,
JSON.stringify(graphqlCollectionsData)
)
}
const translatedRestCollectionsData = restCollectionsData.map(
translateToNewRESTCollection
)
const translatedGraphqlCollectionsData = graphqlCollectionsData.map(
translateToNewGQLCollection
)
setRESTCollections(translatedRestCollectionsData)
setGraphqlCollections(translatedGraphqlCollectionsData)
restCollectionStore.subject$.subscribe(({ state }) => {
window.localStorage.setItem(restCollectionsKey, JSON.stringify(state))
})
graphqlCollectionStore.subject$.subscribe(({ state }) => {
window.localStorage.setItem(graphqlCollectionsKey, JSON.stringify(state))
})
}
private setupEnvironmentsPersistence() {
const environmentsKey = "environments"
let environmentsData: Environment[] = JSON.parse(
window.localStorage.getItem(environmentsKey) || "[]"
)
// Validate data read from localStorage
const result = ENVIRONMENTS_SCHEMA.safeParse(environmentsData)
if (result.success) {
environmentsData = result.data
} else {
this.showErrorToast(environmentsKey)
window.localStorage.setItem(
`${environmentsKey}-backup`,
JSON.stringify(environmentsData)
)
}
// Check if a global env is defined and if so move that to globals
const globalIndex = environmentsData.findIndex(
(x) => x.name.toLowerCase() === "globals"
)
if (globalIndex !== -1) {
const globalEnv = environmentsData[globalIndex]
globalEnv.variables.forEach((variable) => addGlobalEnvVariable(variable))
// Remove global from environments
environmentsData.splice(globalIndex, 1)
// Just sync the changes manually
window.localStorage.setItem(
environmentsKey,
JSON.stringify(environmentsData)
)
}
replaceEnvironments(environmentsData)
environments$.subscribe((envs) => {
window.localStorage.setItem(environmentsKey, JSON.stringify(envs))
})
}
private setupSelectedEnvPersistence() {
const selectedEnvIndexKey = "selectedEnvIndex"
let selectedEnvIndexValue = JSON.parse(
window.localStorage.getItem(selectedEnvIndexKey) ?? "null"
)
// Validate data read from localStorage
const result = SELECTED_ENV_INDEX_SCHEMA.safeParse(selectedEnvIndexValue)
if (result.success) {
selectedEnvIndexValue = result.data
} else {
this.showErrorToast(selectedEnvIndexKey)
window.localStorage.setItem(
`${selectedEnvIndexKey}-backup`,
JSON.stringify(selectedEnvIndexValue)
)
}
// If there is a selected env index, set it to the store else set it to null
if (selectedEnvIndexValue) {
setSelectedEnvironmentIndex(selectedEnvIndexValue)
} else {
setSelectedEnvironmentIndex({
type: "NO_ENV_SELECTED",
})
}
selectedEnvironmentIndex$.subscribe((envIndex) => {
window.localStorage.setItem(selectedEnvIndexKey, JSON.stringify(envIndex))
})
}
private setupWebsocketPersistence() {
const wsRequestKey = "WebsocketRequest"
let wsRequestData = JSON.parse(
window.localStorage.getItem(wsRequestKey) || "null"
)
// Validate data read from localStorage
const result = WEBSOCKET_REQUEST_SCHEMA.safeParse(wsRequestData)
if (result.success) {
wsRequestData = result.data
} else {
this.showErrorToast(wsRequestKey)
window.localStorage.setItem(
`${wsRequestKey}-backup`,
JSON.stringify(wsRequestData)
)
}
setWSRequest(wsRequestData)
WSRequest$.subscribe((req) => {
window.localStorage.setItem(wsRequestKey, JSON.stringify(req))
})
}
private setupSocketIOPersistence() {
const sioRequestKey = "SocketIORequest"
let sioRequestData = JSON.parse(
window.localStorage.getItem(sioRequestKey) || "null"
)
// Validate data read from localStorage
const result = SOCKET_IO_REQUEST_SCHEMA.safeParse(sioRequestData)
if (result.success) {
sioRequestData = result.data
} else {
this.showErrorToast(sioRequestKey)
window.localStorage.setItem(
`${sioRequestKey}-backup`,
JSON.stringify(sioRequestData)
)
}
setSIORequest(sioRequestData)
SIORequest$.subscribe((req) => {
window.localStorage.setItem(sioRequestKey, JSON.stringify(req))
})
}
private setupSSEPersistence() {
const sseRequestKey = "SSERequest"
let sseRequestData = JSON.parse(
window.localStorage.getItem(sseRequestKey) || "null"
)
// Validate data read from localStorage
const result = SSE_REQUEST_SCHEMA.safeParse(sseRequestData)
if (result.success) {
sseRequestData = result.data
} else {
this.showErrorToast(sseRequestKey)
window.localStorage.setItem(
`${sseRequestKey}-backup`,
JSON.stringify(sseRequestData)
)
}
setSSERequest(sseRequestData)
SSERequest$.subscribe((req) => {
window.localStorage.setItem(sseRequestKey, JSON.stringify(req))
})
}
private setupMQTTPersistence() {
const mqttRequestKey = "MQTTRequest"
let mqttRequestData = JSON.parse(
window.localStorage.getItem(mqttRequestKey) || "null"
)
// Validate data read from localStorage
const result = MQTT_REQUEST_SCHEMA.safeParse(mqttRequestData)
if (result.success) {
mqttRequestData = result.data
} else {
this.showErrorToast(mqttRequestKey)
window.localStorage.setItem(
`${mqttRequestKey}-backup`,
JSON.stringify(mqttRequestData)
)
}
setMQTTRequest(mqttRequestData)
MQTTRequest$.subscribe((req) => {
window.localStorage.setItem(mqttRequestKey, JSON.stringify(req))
})
}
private setupGlobalEnvsPersistence() {
const globalEnvKey = "globalEnv"
let globalEnvData: Environment["variables"] = JSON.parse(
window.localStorage.getItem(globalEnvKey) || "[]"
)
// Validate data read from localStorage
const result = GLOBAL_ENV_SCHEMA.safeParse(globalEnvData)
if (result.success) {
globalEnvData = result.data
} else {
this.showErrorToast(globalEnvKey)
window.localStorage.setItem(
`${globalEnvKey}-backup`,
JSON.stringify(globalEnvData)
)
}
setGlobalEnvVariables(globalEnvData)
globalEnv$.subscribe((vars) => {
window.localStorage.setItem(globalEnvKey, JSON.stringify(vars))
})
}
private setupGQLTabsPersistence() {
const gqlTabStateKey = "gqlTabState"
const gqlTabStateData = window.localStorage.getItem(gqlTabStateKey)
try {
if (gqlTabStateData) {
let parsedGqlTabStateData = JSON.parse(gqlTabStateData)
// Validate data read from localStorage
const result = GQL_TAB_STATE_SCHEMA.safeParse(parsedGqlTabStateData)
if (result.success) {
parsedGqlTabStateData = result.data
} else {
this.showErrorToast(gqlTabStateKey)
window.localStorage.setItem(
`${gqlTabStateKey}-backup`,
JSON.stringify(parsedGqlTabStateData)
)
}
this.gqlTabService.loadTabsFromPersistedState(parsedGqlTabStateData)
}
} catch (e) {
console.error(
`Failed parsing persisted tab state, state:`,
gqlTabStateData
)
}
watchDebounced(
this.gqlTabService.persistableTabState,
(newGqlTabStateData) => {
window.localStorage.setItem(
gqlTabStateKey,
JSON.stringify(newGqlTabStateData)
)
},
{ debounce: 500, deep: true }
)
}
private setupRESTTabsPersistence() {
const restTabStateKey = "restTabState"
const restTabStateData = window.localStorage.getItem(restTabStateKey)
try {
if (restTabStateData) {
let parsedGqlTabStateData = JSON.parse(restTabStateData)
// Validate data read from localStorage
const result = REST_TAB_STATE_SCHEMA.safeParse(parsedGqlTabStateData)
if (result.success) {
parsedGqlTabStateData = result.data
} else {
this.showErrorToast(restTabStateKey)
window.localStorage.setItem(
`${restTabStateKey}-backup`,
JSON.stringify(parsedGqlTabStateData)
)
}
this.restTabService.loadTabsFromPersistedState(parsedGqlTabStateData)
}
} catch (e) {
console.error(
`Failed parsing persisted tab state, state:`,
restTabStateData
)
}
watchDebounced(
this.restTabService.persistableTabState,
(newRestTabStateData) => {
window.localStorage.setItem(
restTabStateKey,
JSON.stringify(newRestTabStateData)
)
},
{ debounce: 500, deep: true }
)
}
public setupLocalPersistence() {
this.checkAndMigrateOldSettings()
this.setupLocalStatePersistence()
this.setupSettingsPersistence()
this.setupRESTTabsPersistence()
this.setupGQLTabsPersistence()
this.setupHistoryPersistence()
this.setupCollectionsPersistence()
this.setupGlobalEnvsPersistence()
this.setupEnvironmentsPersistence()
this.setupSelectedEnvPersistence()
this.setupWebsocketPersistence()
this.setupSocketIOPersistence()
this.setupSSEPersistence()
this.setupMQTTPersistence()
}
/**
* Gets a value from localStorage
*
* NOTE: Use localStorage to only store non-reactive simple data
* For more complex data, use stores and connect it to `PersistenceService`
*/
public getLocalConfig(name: string) {
return window.localStorage.getItem(name)
}
/**
* Sets a value in localStorage
*
* NOTE: Use localStorage to only store non-reactive simple data
* For more complex data, use stores and connect it to `PersistenceService`
*/
public setLocalConfig(key: string, value: string) {
window.localStorage.setItem(key, value)
}
/**
* Clear config value in localStorage
*/
public removeLocalConfig(key: string) {
window.localStorage.removeItem(key)
}
}

View File

@@ -0,0 +1,470 @@
import {
Environment,
GQLHeader,
HoppGQLAuth,
HoppGQLRequest,
HoppRESTRequest,
} from "@hoppscotch/data"
import { entityReference } from "verzod"
import { z } from "zod"
import { HoppAccentColors, HoppBgColors } from "~/newstore/settings"
const ThemeColorSchema = z.enum([
"green",
"teal",
"blue",
"indigo",
"purple",
"yellow",
"orange",
"red",
"pink",
])
const BgColorSchema = z.enum(["system", "light", "dark", "black"])
const SettingsDefSchema = z.object({
syncCollections: z.boolean(),
syncHistory: z.boolean(),
syncEnvironments: z.boolean(),
PROXY_URL: z.string(),
CURRENT_INTERCEPTOR_ID: z.string(),
URL_EXCLUDES: z.object({
auth: z.boolean(),
httpUser: z.boolean(),
httpPassword: z.boolean(),
bearerToken: z.boolean(),
oauth2Token: z.boolean(),
}),
THEME_COLOR: ThemeColorSchema,
BG_COLOR: BgColorSchema,
TELEMETRY_ENABLED: z.boolean(),
EXPAND_NAVIGATION: z.boolean(),
SIDEBAR: z.boolean(),
SIDEBAR_ON_LEFT: z.boolean(),
COLUMN_LAYOUT: z.boolean(),
})
// Common properties shared across REST & GQL collections
const HoppCollectionSchemaCommonProps = z
.object({
v: z.number(),
name: z.string(),
id: z.optional(z.string()),
})
.strict()
const HoppRESTRequestSchema = entityReference(HoppRESTRequest)
const HoppGQLRequestSchema = entityReference(HoppGQLRequest)
// @ts-expect-error recursive schema
const HoppRESTCollectionSchema = HoppCollectionSchemaCommonProps.extend({
folders: z.array(z.lazy(() => HoppRESTCollectionSchema)),
requests: z.optional(z.array(HoppRESTRequestSchema)),
}).strict()
// @ts-expect-error recursive schema
const HoppGQLCollectionSchema = HoppCollectionSchemaCommonProps.extend({
folders: z.array(z.lazy(() => HoppGQLCollectionSchema)),
requests: z.optional(z.array(HoppGQLRequestSchema)),
}).strict()
export const VUEX_SCHEMA = z.object({
postwoman: z.optional(
z.object({
settings: z.optional(SettingsDefSchema),
//! Versioned entities
collections: z.optional(z.array(HoppRESTCollectionSchema)),
collectionsGraphql: z.optional(z.array(HoppGQLCollectionSchema)),
environments: z.optional(z.array(entityReference(Environment))),
})
),
})
export const THEME_COLOR_SCHEMA = z.enum(HoppAccentColors)
export const NUXT_COLOR_MODE_SCHEMA = z.enum(HoppBgColors)
export const LOCAL_STATE_SCHEMA = z.union([
z.object({}).strict(),
z
.object({
REMEMBERED_TEAM_ID: z.optional(z.string()),
})
.strict(),
])
export const SETTINGS_SCHEMA = z.union([
z.object({}).strict(),
SettingsDefSchema.extend({
EXTENSIONS_ENABLED: z.optional(z.boolean()),
PROXY_ENABLED: z.optional(z.boolean()),
}),
])
export const REST_HISTORY_ENTRY_SCHEMA = z
.object({
v: z.number(),
//! Versioned entity
request: HoppRESTRequestSchema,
responseMeta: z
.object({
duration: z.nullable(z.number()),
statusCode: z.nullable(z.number()),
})
.strict(),
star: z.boolean(),
id: z.optional(z.string()),
updatedOn: z.optional(z.union([z.date(), z.string()])),
})
.strict()
export const GQL_HISTORY_ENTRY_SCHEMA = z
.object({
v: z.number(),
//! Versioned entity
request: HoppGQLRequestSchema,
response: z.string(),
star: z.boolean(),
id: z.optional(z.string()),
updatedOn: z.optional(z.union([z.date(), z.string()])),
})
.strict()
export const REST_COLLECTION_SCHEMA = HoppRESTCollectionSchema
export const GQL_COLLECTION_SCHEMA = HoppGQLCollectionSchema
export const ENVIRONMENTS_SCHEMA = z.array(entityReference(Environment))
export const SELECTED_ENV_INDEX_SCHEMA = z.nullable(
z.discriminatedUnion("type", [
z
.object({
type: z.literal("NO_ENV_SELECTED"),
})
.strict(),
z
.object({
type: z.literal("MY_ENV"),
index: z.number(),
})
.strict(),
z.object({
type: z.literal("TEAM_ENV"),
teamID: z.string(),
teamEnvID: z.string(),
// ! Versioned entity
environment: entityReference(Environment),
}),
])
)
export const WEBSOCKET_REQUEST_SCHEMA = z.nullable(
z
.object({
endpoint: z.string(),
protocols: z.array(
z
.object({
value: z.string(),
active: z.boolean(),
})
.strict()
),
})
.strict()
)
export const SOCKET_IO_REQUEST_SCHEMA = z.nullable(
z
.object({
endpoint: z.string(),
path: z.string(),
version: z.union([z.literal("v4"), z.literal("v3"), z.literal("v2")]),
})
.strict()
)
export const SSE_REQUEST_SCHEMA = z.nullable(
z
.object({
endpoint: z.string(),
eventType: z.string(),
})
.strict()
)
export const MQTT_REQUEST_SCHEMA = z.nullable(
z
.object({
endpoint: z.string(),
clientID: z.string(),
})
.strict()
)
export const GLOBAL_ENV_SCHEMA = z.union([
z.array(z.never()),
z.array(
z
.object({
key: z.string(),
value: z.string(),
})
.strict()
),
])
const OperationTypeSchema = z.enum([
"subscription",
"query",
"mutation",
"teardown",
])
const RunQueryOptionsSchema = z
.object({
name: z.optional(z.string()),
url: z.string(),
headers: z.array(GQLHeader),
query: z.string(),
variables: z.string(),
auth: HoppGQLAuth,
operationName: z.optional(z.string()),
operationType: OperationTypeSchema,
})
.strict()
const HoppGQLSaveContextSchema = z.nullable(
z.discriminatedUnion("originLocation", [
z
.object({
originLocation: z.literal("user-collection"),
folderPath: z.string(),
requestIndex: z.number(),
})
.strict(),
z
.object({
originLocation: z.literal("team-collection"),
requestID: z.string(),
teamID: z.optional(z.string()),
collectionID: z.optional(z.string()),
})
.strict(),
])
)
const GQLResponseEventSchema = z.array(
z
.object({
time: z.number(),
operationName: z.optional(z.string()),
operationType: OperationTypeSchema,
data: z.string(),
rawQuery: z.optional(RunQueryOptionsSchema),
})
.strict()
)
const validGqlOperations = [
"query",
"headers",
"variables",
"authorization",
] as const
export const GQL_TAB_STATE_SCHEMA = z
.object({
lastActiveTabID: z.string(),
orderedDocs: z.array(
z.object({
tabID: z.string(),
doc: z
.object({
// Versioned entity
request: entityReference(HoppGQLRequest),
isDirty: z.boolean(),
saveContext: z.optional(HoppGQLSaveContextSchema),
response: z.optional(z.nullable(GQLResponseEventSchema)),
responseTabPreference: z.optional(z.string()),
optionTabPreference: z.optional(z.enum(validGqlOperations)),
})
.strict(),
})
),
})
.strict()
const HoppTestExpectResultSchema = z
.object({
status: z.enum(["fail", "pass", "error"]),
message: z.string(),
})
.strict()
// @ts-expect-error recursive schema
const HoppTestDataSchema = z.lazy(() =>
z
.object({
description: z.string(),
expectResults: z.array(HoppTestExpectResultSchema),
tests: z.array(HoppTestDataSchema),
})
.strict()
)
const EnvironmentVariablesSchema = z
.object({
key: z.string(),
value: z.string(),
})
.strict()
const HoppTestResultSchema = z
.object({
tests: z.array(HoppTestDataSchema),
expectResults: z.array(HoppTestExpectResultSchema),
description: z.string(),
scriptError: z.boolean(),
envDiff: z
.object({
global: z
.object({
additions: z.array(EnvironmentVariablesSchema),
updations: z.array(
EnvironmentVariablesSchema.extend({ previousValue: z.string() })
),
deletions: z.array(EnvironmentVariablesSchema),
})
.strict(),
selected: z
.object({
additions: z.array(EnvironmentVariablesSchema),
updations: z.array(
EnvironmentVariablesSchema.extend({ previousValue: z.string() })
),
deletions: z.array(EnvironmentVariablesSchema),
})
.strict(),
})
.strict(),
})
.strict()
const HoppRESTResponseHeaderSchema = z
.object({
key: z.string(),
value: z.string(),
})
.strict()
const HoppRESTResponseSchema = z.discriminatedUnion("type", [
z
.object({
type: z.literal("loading"),
// !Versioned entity
req: HoppRESTRequestSchema,
})
.strict(),
z
.object({
type: z.literal("fail"),
headers: z.array(HoppRESTResponseHeaderSchema),
body: z.instanceof(ArrayBuffer),
statusCode: z.number(),
meta: z
.object({
responseSize: z.number(),
responseDuration: z.number(),
})
.strict(),
// !Versioned entity
req: HoppRESTRequestSchema,
})
.strict(),
z
.object({
type: z.literal("network_fail"),
error: z.unknown(),
// !Versioned entity
req: HoppRESTRequestSchema,
})
.strict(),
z
.object({
type: z.literal("script_fail"),
error: z.instanceof(Error),
})
.strict(),
z
.object({
type: z.literal("success"),
headers: z.array(HoppRESTResponseHeaderSchema),
body: z.instanceof(ArrayBuffer),
statusCode: z.number(),
meta: z
.object({
responseSize: z.number(),
responseDuration: z.number(),
})
.strict(),
// !Versioned entity
req: HoppRESTRequestSchema,
})
.strict(),
])
const HoppRESTSaveContextSchema = z.nullable(
z.discriminatedUnion("originLocation", [
z
.object({
originLocation: z.literal("user-collection"),
folderPath: z.string(),
requestIndex: z.number(),
})
.strict(),
z
.object({
originLocation: z.literal("team-collection"),
requestID: z.string(),
teamID: z.optional(z.string()),
collectionID: z.optional(z.string()),
})
.strict(),
])
)
const validRestOperations = [
"params",
"bodyParams",
"headers",
"authorization",
"preRequestScript",
"tests",
] as const
export const REST_TAB_STATE_SCHEMA = z
.object({
lastActiveTabID: z.string(),
orderedDocs: z.array(
z.object({
tabID: z.string(),
doc: z
.object({
// !Versioned entity
request: entityReference(HoppRESTRequest),
isDirty: z.boolean(),
saveContext: z.optional(HoppRESTSaveContextSchema),
response: z.optional(z.nullable(HoppRESTResponseSchema)),
testResults: z.optional(z.nullable(HoppTestResultSchema)),
responseTabPreference: z.optional(z.string()),
optionTabPreference: z.optional(z.enum(validRestOperations)),
})
.strict(),
})
),
})
.strict()