chore: split app to commons and web (squash commit)
This commit is contained in:
73
packages/hoppscotch-common/src/newstore/DispatchingStore.ts
Normal file
73
packages/hoppscotch-common/src/newstore/DispatchingStore.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Subject, BehaviorSubject } from "rxjs"
|
||||
import { map } from "rxjs/operators"
|
||||
import { assign, clone } from "lodash-es"
|
||||
|
||||
type dispatcherFunc<StoreType> = (
|
||||
currentVal: StoreType,
|
||||
payload: any
|
||||
) => Partial<StoreType>
|
||||
|
||||
/**
|
||||
* Defines a dispatcher.
|
||||
*
|
||||
* This function exists to provide better typing for dispatch function.
|
||||
* As you can see, its pretty much an identity function.
|
||||
*/
|
||||
export const defineDispatchers = <StoreType, T>(
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
dispatchers: { [_ in keyof T]: dispatcherFunc<StoreType> }
|
||||
) => dispatchers
|
||||
|
||||
type Dispatch<
|
||||
StoreType,
|
||||
DispatchersType extends Record<string, dispatcherFunc<StoreType>>
|
||||
> = {
|
||||
dispatcher: keyof DispatchersType
|
||||
payload: any
|
||||
}
|
||||
|
||||
export default class DispatchingStore<
|
||||
StoreType,
|
||||
DispatchersType extends Record<string, dispatcherFunc<StoreType>>
|
||||
> {
|
||||
#state$: BehaviorSubject<StoreType>
|
||||
#dispatchers: DispatchersType
|
||||
#dispatches$: Subject<Dispatch<StoreType, DispatchersType>> = new Subject()
|
||||
|
||||
constructor(initialValue: StoreType, dispatchers: DispatchersType) {
|
||||
this.#state$ = new BehaviorSubject(initialValue)
|
||||
this.#dispatchers = dispatchers
|
||||
|
||||
this.#dispatches$
|
||||
.pipe(
|
||||
map(({ dispatcher, payload }) =>
|
||||
this.#dispatchers[dispatcher](this.value, payload)
|
||||
)
|
||||
)
|
||||
.subscribe((val) => {
|
||||
const data = clone(this.value)
|
||||
assign(data, val)
|
||||
|
||||
this.#state$.next(data)
|
||||
})
|
||||
}
|
||||
|
||||
get subject$() {
|
||||
return this.#state$
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this.subject$.value
|
||||
}
|
||||
|
||||
get dispatches$() {
|
||||
return this.#dispatches$
|
||||
}
|
||||
|
||||
dispatch({ dispatcher, payload }: Dispatch<StoreType, DispatchersType>) {
|
||||
if (!this.#dispatchers[dispatcher])
|
||||
throw new Error(`Undefined dispatch type '${dispatcher}'`)
|
||||
|
||||
this.#dispatches$.next({ dispatcher, payload })
|
||||
}
|
||||
}
|
||||
281
packages/hoppscotch-common/src/newstore/GQLSession.ts
Normal file
281
packages/hoppscotch-common/src/newstore/GQLSession.ts
Normal file
@@ -0,0 +1,281 @@
|
||||
import { distinctUntilChanged, pluck } from "rxjs/operators"
|
||||
import {
|
||||
GQLHeader,
|
||||
HoppGQLRequest,
|
||||
makeGQLRequest,
|
||||
HoppGQLAuth,
|
||||
} from "@hoppscotch/data"
|
||||
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
|
||||
import { useStream } from "@composables/stream"
|
||||
|
||||
type GQLSession = {
|
||||
request: HoppGQLRequest
|
||||
schema: string
|
||||
response: string
|
||||
}
|
||||
|
||||
export const defaultGQLSession: GQLSession = {
|
||||
request: makeGQLRequest({
|
||||
name: "Untitled request",
|
||||
url: "https://echo.hoppscotch.io/graphql",
|
||||
headers: [],
|
||||
variables: `{
|
||||
"id": "1"
|
||||
}`,
|
||||
query: `query Request {
|
||||
method
|
||||
url
|
||||
headers {
|
||||
key
|
||||
value
|
||||
}
|
||||
}
|
||||
`,
|
||||
auth: {
|
||||
authType: "none",
|
||||
authActive: true,
|
||||
},
|
||||
}),
|
||||
schema: "",
|
||||
response: "",
|
||||
}
|
||||
|
||||
const dispatchers = defineDispatchers({
|
||||
setSession(_: GQLSession, { session }: { session: GQLSession }) {
|
||||
return session
|
||||
},
|
||||
setName(curr: GQLSession, { newName }: { newName: string }) {
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
name: newName,
|
||||
},
|
||||
}
|
||||
},
|
||||
setURL(curr: GQLSession, { newURL }: { newURL: string }) {
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
url: newURL,
|
||||
},
|
||||
}
|
||||
},
|
||||
setHeaders(curr: GQLSession, { headers }: { headers: GQLHeader[] }) {
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
headers,
|
||||
},
|
||||
}
|
||||
},
|
||||
addHeader(curr: GQLSession, { header }: { header: GQLHeader }) {
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
headers: [...curr.request.headers, header],
|
||||
},
|
||||
}
|
||||
},
|
||||
removeHeader(curr: GQLSession, { headerIndex }: { headerIndex: number }) {
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
headers: curr.request.headers.filter((_x, i) => i !== headerIndex),
|
||||
},
|
||||
}
|
||||
},
|
||||
updateHeader(
|
||||
curr: GQLSession,
|
||||
{
|
||||
headerIndex,
|
||||
updatedHeader,
|
||||
}: { headerIndex: number; updatedHeader: GQLHeader }
|
||||
) {
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
headers: curr.request.headers.map((x, i) =>
|
||||
i === headerIndex ? updatedHeader : x
|
||||
),
|
||||
},
|
||||
}
|
||||
},
|
||||
setQuery(curr: GQLSession, { newQuery }: { newQuery: string }) {
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
query: newQuery,
|
||||
},
|
||||
}
|
||||
},
|
||||
setVariables(curr: GQLSession, { newVariables }: { newVariables: string }) {
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
variables: newVariables,
|
||||
},
|
||||
}
|
||||
},
|
||||
setResponse(_: GQLSession, { newResponse }: { newResponse: string }) {
|
||||
return {
|
||||
response: newResponse,
|
||||
}
|
||||
},
|
||||
setAuth(curr: GQLSession, { newAuth }: { newAuth: HoppGQLAuth }) {
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
auth: newAuth,
|
||||
},
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const gqlSessionStore = new DispatchingStore(
|
||||
defaultGQLSession,
|
||||
dispatchers
|
||||
)
|
||||
|
||||
export function setGQLURL(newURL: string) {
|
||||
gqlSessionStore.dispatch({
|
||||
dispatcher: "setURL",
|
||||
payload: {
|
||||
newURL,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setGQLHeaders(headers: GQLHeader[]) {
|
||||
gqlSessionStore.dispatch({
|
||||
dispatcher: "setHeaders",
|
||||
payload: {
|
||||
headers,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function addGQLHeader(header: GQLHeader) {
|
||||
gqlSessionStore.dispatch({
|
||||
dispatcher: "addHeader",
|
||||
payload: {
|
||||
header,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function updateGQLHeader(headerIndex: number, updatedHeader: GQLHeader) {
|
||||
gqlSessionStore.dispatch({
|
||||
dispatcher: "updateHeader",
|
||||
payload: {
|
||||
headerIndex,
|
||||
updatedHeader,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function removeGQLHeader(headerIndex: number) {
|
||||
gqlSessionStore.dispatch({
|
||||
dispatcher: "removeHeader",
|
||||
payload: {
|
||||
headerIndex,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function clearGQLHeaders() {
|
||||
gqlSessionStore.dispatch({
|
||||
dispatcher: "setHeaders",
|
||||
payload: {
|
||||
headers: [],
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setGQLQuery(newQuery: string) {
|
||||
gqlSessionStore.dispatch({
|
||||
dispatcher: "setQuery",
|
||||
payload: {
|
||||
newQuery,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setGQLVariables(newVariables: string) {
|
||||
gqlSessionStore.dispatch({
|
||||
dispatcher: "setVariables",
|
||||
payload: {
|
||||
newVariables,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setGQLResponse(newResponse: string) {
|
||||
gqlSessionStore.dispatch({
|
||||
dispatcher: "setResponse",
|
||||
payload: {
|
||||
newResponse,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function getGQLSession() {
|
||||
return gqlSessionStore.value
|
||||
}
|
||||
|
||||
export function setGQLSession(session: GQLSession) {
|
||||
gqlSessionStore.dispatch({
|
||||
dispatcher: "setSession",
|
||||
payload: {
|
||||
session,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function useGQLRequestName() {
|
||||
return useStream(gqlName$, gqlSessionStore.value.request.name, (newName) => {
|
||||
gqlSessionStore.dispatch({
|
||||
dispatcher: "setName",
|
||||
payload: { newName },
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export function setGQLAuth(newAuth: HoppGQLAuth) {
|
||||
gqlSessionStore.dispatch({
|
||||
dispatcher: "setAuth",
|
||||
payload: {
|
||||
newAuth,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const gqlName$ = gqlSessionStore.subject$.pipe(
|
||||
pluck("request", "name"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
export const gqlURL$ = gqlSessionStore.subject$.pipe(
|
||||
pluck("request", "url"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
export const gqlQuery$ = gqlSessionStore.subject$.pipe(
|
||||
pluck("request", "query"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
export const gqlVariables$ = gqlSessionStore.subject$.pipe(
|
||||
pluck("request", "variables"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
export const gqlHeaders$ = gqlSessionStore.subject$.pipe(
|
||||
pluck("request", "headers"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const gqlResponse$ = gqlSessionStore.subject$.pipe(
|
||||
pluck("response"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const gqlAuth$ = gqlSessionStore.subject$.pipe(
|
||||
pluck("request", "auth"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
40
packages/hoppscotch-common/src/newstore/HoppExtension.ts
Normal file
40
packages/hoppscotch-common/src/newstore/HoppExtension.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { distinctUntilChanged, pluck } from "rxjs"
|
||||
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
|
||||
|
||||
export type ExtensionStatus = "available" | "unknown-origin" | "waiting"
|
||||
|
||||
type InitialState = {
|
||||
extensionStatus: ExtensionStatus
|
||||
}
|
||||
|
||||
const initialState: InitialState = {
|
||||
extensionStatus: "waiting",
|
||||
}
|
||||
|
||||
const dispatchers = defineDispatchers({
|
||||
changeExtensionStatus(
|
||||
_,
|
||||
{ extensionStatus }: { extensionStatus: ExtensionStatus }
|
||||
) {
|
||||
return {
|
||||
extensionStatus,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const hoppExtensionStore = new DispatchingStore(
|
||||
initialState,
|
||||
dispatchers
|
||||
)
|
||||
|
||||
export const extensionStatus$ = hoppExtensionStore.subject$.pipe(
|
||||
pluck("extensionStatus"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export function changeExtensionStatus(extensionStatus: ExtensionStatus) {
|
||||
hoppExtensionStore.dispatch({
|
||||
dispatcher: "changeExtensionStatus",
|
||||
payload: { extensionStatus },
|
||||
})
|
||||
}
|
||||
304
packages/hoppscotch-common/src/newstore/MQTTSession.ts
Normal file
304
packages/hoppscotch-common/src/newstore/MQTTSession.ts
Normal file
@@ -0,0 +1,304 @@
|
||||
import { distinctUntilChanged, pluck } from "rxjs/operators"
|
||||
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
|
||||
import { MQTTConnection } from "~/helpers/realtime/MQTTConnection"
|
||||
import {
|
||||
HoppRealtimeLog,
|
||||
HoppRealtimeLogLine,
|
||||
} from "~/helpers/types/HoppRealtimeLog"
|
||||
|
||||
type MQTTTab = {
|
||||
id: string
|
||||
name: string
|
||||
color: string
|
||||
removable: boolean
|
||||
logs: HoppRealtimeLog[]
|
||||
}
|
||||
|
||||
type HoppMQTTRequest = {
|
||||
endpoint: string
|
||||
clientID: string
|
||||
}
|
||||
|
||||
type HoppMQTTSession = {
|
||||
request: HoppMQTTRequest
|
||||
subscriptionState: boolean
|
||||
log: HoppRealtimeLog
|
||||
socket: MQTTConnection
|
||||
tabs: MQTTTab[]
|
||||
currentTabId: string
|
||||
}
|
||||
|
||||
const defaultMQTTRequest: HoppMQTTRequest = {
|
||||
endpoint: "wss://test.mosquitto.org:8081",
|
||||
clientID: "hoppscotch",
|
||||
}
|
||||
|
||||
const defaultTab: MQTTTab = {
|
||||
id: "all",
|
||||
name: "All Topics",
|
||||
color: "var(--accent-color)",
|
||||
removable: false,
|
||||
logs: [],
|
||||
}
|
||||
|
||||
const defaultMQTTSession: HoppMQTTSession = {
|
||||
request: defaultMQTTRequest,
|
||||
subscriptionState: false,
|
||||
socket: new MQTTConnection(),
|
||||
log: [],
|
||||
tabs: [defaultTab],
|
||||
currentTabId: defaultTab.id,
|
||||
}
|
||||
|
||||
const dispatchers = defineDispatchers({
|
||||
setRequest(
|
||||
_: HoppMQTTSession,
|
||||
{ newRequest }: { newRequest: HoppMQTTRequest }
|
||||
) {
|
||||
return {
|
||||
request: newRequest,
|
||||
}
|
||||
},
|
||||
setEndpoint(curr: HoppMQTTSession, { newEndpoint }: { newEndpoint: string }) {
|
||||
return {
|
||||
request: {
|
||||
clientID: curr.request.clientID,
|
||||
endpoint: newEndpoint,
|
||||
},
|
||||
}
|
||||
},
|
||||
setClientID(curr: HoppMQTTSession, { newClientID }: { newClientID: string }) {
|
||||
return {
|
||||
request: {
|
||||
endpoint: curr.request.endpoint,
|
||||
clientID: newClientID,
|
||||
},
|
||||
}
|
||||
},
|
||||
setConn(_: HoppMQTTSession, { socket }: { socket: MQTTConnection }) {
|
||||
return {
|
||||
socket,
|
||||
}
|
||||
},
|
||||
setSubscriptionState(_: HoppMQTTSession, { state }: { state: boolean }) {
|
||||
return {
|
||||
subscriptionState: state,
|
||||
}
|
||||
},
|
||||
setLog(_: HoppMQTTSession, { log }: { log: HoppRealtimeLog }) {
|
||||
return {
|
||||
log,
|
||||
}
|
||||
},
|
||||
addLogLine(curr: HoppMQTTSession, { line }: { line: HoppRealtimeLogLine }) {
|
||||
return {
|
||||
log: [...curr.log, line],
|
||||
}
|
||||
},
|
||||
setTabs(_: HoppMQTTSession, { tabs }: { tabs: MQTTTab[] }) {
|
||||
return {
|
||||
tabs,
|
||||
}
|
||||
},
|
||||
addTab(curr: HoppMQTTSession, { tab }: { tab: MQTTTab }) {
|
||||
return {
|
||||
tabs: [...curr.tabs, tab],
|
||||
}
|
||||
},
|
||||
setCurrentTabId(_: HoppMQTTSession, { tabId }: { tabId: string }) {
|
||||
return {
|
||||
currentTabId: tabId,
|
||||
}
|
||||
},
|
||||
setCurrentTabLog(
|
||||
_: HoppMQTTSession,
|
||||
{ log, tabId }: { log: HoppRealtimeLog[]; tabId: string }
|
||||
) {
|
||||
const newTabs = _.tabs.map((tab) => {
|
||||
if (tab.id === tabId) tab.logs = log
|
||||
return tab
|
||||
})
|
||||
|
||||
return {
|
||||
tabs: newTabs,
|
||||
}
|
||||
},
|
||||
addCurrentTabLogLine(
|
||||
_: HoppMQTTSession,
|
||||
{ line, tabId }: { tabId: string; line: HoppRealtimeLog }
|
||||
) {
|
||||
const newTabs = _.tabs.map((tab) => {
|
||||
if (tab.id === tabId) tab.logs = [...tab.logs, line]
|
||||
return tab
|
||||
})
|
||||
|
||||
return {
|
||||
tabs: newTabs,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const MQTTSessionStore = new DispatchingStore(defaultMQTTSession, dispatchers)
|
||||
|
||||
export function setMQTTRequest(newRequest?: HoppMQTTRequest) {
|
||||
MQTTSessionStore.dispatch({
|
||||
dispatcher: "setRequest",
|
||||
payload: {
|
||||
newRequest: newRequest ?? defaultMQTTRequest,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setMQTTEndpoint(newEndpoint: string) {
|
||||
MQTTSessionStore.dispatch({
|
||||
dispatcher: "setEndpoint",
|
||||
payload: {
|
||||
newEndpoint,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setMQTTClientID(newClientID: string) {
|
||||
MQTTSessionStore.dispatch({
|
||||
dispatcher: "setClientID",
|
||||
payload: {
|
||||
newClientID,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setMQTTConn(socket: MQTTConnection) {
|
||||
MQTTSessionStore.dispatch({
|
||||
dispatcher: "setConn",
|
||||
payload: {
|
||||
socket,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setMQTTSubscriptionState(state: boolean) {
|
||||
MQTTSessionStore.dispatch({
|
||||
dispatcher: "setSubscriptionState",
|
||||
payload: {
|
||||
state,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setMQTTLog(log: HoppRealtimeLog) {
|
||||
MQTTSessionStore.dispatch({
|
||||
dispatcher: "setLog",
|
||||
payload: {
|
||||
log,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function addMQTTLogLine(line: HoppRealtimeLogLine) {
|
||||
MQTTSessionStore.dispatch({
|
||||
dispatcher: "addLogLine",
|
||||
payload: {
|
||||
line,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setMQTTTabs(tabs: MQTTTab[]) {
|
||||
MQTTSessionStore.dispatch({
|
||||
dispatcher: "setTabs",
|
||||
payload: {
|
||||
tabs,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function addMQTTTab(tab: MQTTTab) {
|
||||
MQTTSessionStore.dispatch({
|
||||
dispatcher: "addTab",
|
||||
payload: {
|
||||
tab,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setCurrentTab(tabId: string) {
|
||||
MQTTSessionStore.dispatch({
|
||||
dispatcher: "setCurrentTabId",
|
||||
payload: {
|
||||
tabId,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setMQTTCurrentTabLog(tabId: string, log: HoppRealtimeLog) {
|
||||
MQTTSessionStore.dispatch({
|
||||
dispatcher: "setCurrentTabLog",
|
||||
payload: {
|
||||
tabId,
|
||||
log,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function addMQTTCurrentTabLogLine(
|
||||
tabId: string,
|
||||
line: HoppRealtimeLogLine
|
||||
) {
|
||||
MQTTSessionStore.dispatch({
|
||||
dispatcher: "addCurrentTabLogLine",
|
||||
payload: {
|
||||
tabId,
|
||||
line,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const MQTTRequest$ = MQTTSessionStore.subject$.pipe(
|
||||
pluck("request"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const MQTTEndpoint$ = MQTTSessionStore.subject$.pipe(
|
||||
pluck("request", "endpoint"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const MQTTClientID$ = MQTTSessionStore.subject$.pipe(
|
||||
pluck("request", "clientID"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const MQTTConnectingState$ = MQTTSessionStore.subject$.pipe(
|
||||
pluck("connectingState"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const MQTTConnectionState$ = MQTTSessionStore.subject$.pipe(
|
||||
pluck("connectionState"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const MQTTSubscriptionState$ = MQTTSessionStore.subject$.pipe(
|
||||
pluck("subscriptionState"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const MQTTConn$ = MQTTSessionStore.subject$.pipe(
|
||||
pluck("socket"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const MQTTLog$ = MQTTSessionStore.subject$.pipe(
|
||||
pluck("log"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const MQTTTabs$ = MQTTSessionStore.subject$.pipe(
|
||||
pluck("tabs"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const MQTTCurrentTab$ = MQTTSessionStore.subject$.pipe(
|
||||
pluck("currentTabId"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
710
packages/hoppscotch-common/src/newstore/RESTSession.ts
Normal file
710
packages/hoppscotch-common/src/newstore/RESTSession.ts
Normal file
@@ -0,0 +1,710 @@
|
||||
import { pluck, distinctUntilChanged, map, filter } from "rxjs/operators"
|
||||
import { Ref } from "vue"
|
||||
import {
|
||||
FormDataKeyValue,
|
||||
HoppRESTHeader,
|
||||
HoppRESTParam,
|
||||
HoppRESTReqBody,
|
||||
HoppRESTRequest,
|
||||
RESTReqSchemaVersion,
|
||||
HoppRESTAuth,
|
||||
ValidContentTypes,
|
||||
} from "@hoppscotch/data"
|
||||
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
|
||||
import { HoppRESTResponse } from "~/helpers/types/HoppRESTResponse"
|
||||
import { useStream } from "@composables/stream"
|
||||
import { HoppTestResult } from "~/helpers/types/HoppTestResult"
|
||||
import { HoppRequestSaveContext } from "~/helpers/types/HoppRequestSaveContext"
|
||||
import { applyBodyTransition } from "~/helpers/rules/BodyTransition"
|
||||
|
||||
type RESTSession = {
|
||||
request: HoppRESTRequest
|
||||
response: HoppRESTResponse | null
|
||||
testResults: HoppTestResult | null
|
||||
saveContext: HoppRequestSaveContext | null
|
||||
}
|
||||
|
||||
export const getDefaultRESTRequest = (): HoppRESTRequest => ({
|
||||
v: RESTReqSchemaVersion,
|
||||
endpoint: "https://echo.hoppscotch.io",
|
||||
name: "Untitled request",
|
||||
params: [],
|
||||
headers: [],
|
||||
method: "GET",
|
||||
auth: {
|
||||
authType: "none",
|
||||
authActive: true,
|
||||
},
|
||||
preRequestScript: "",
|
||||
testScript: "",
|
||||
body: {
|
||||
contentType: null,
|
||||
body: null,
|
||||
},
|
||||
})
|
||||
|
||||
const defaultRESTSession: RESTSession = {
|
||||
request: getDefaultRESTRequest(),
|
||||
response: null,
|
||||
testResults: null,
|
||||
saveContext: null,
|
||||
}
|
||||
|
||||
const dispatchers = defineDispatchers({
|
||||
setRequest(_: RESTSession, { req }: { req: HoppRESTRequest }) {
|
||||
return {
|
||||
request: req,
|
||||
}
|
||||
},
|
||||
setRequestName(curr: RESTSession, { newName }: { newName: string }) {
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
name: newName,
|
||||
},
|
||||
}
|
||||
},
|
||||
setEndpoint(curr: RESTSession, { newEndpoint }: { newEndpoint: string }) {
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
endpoint: newEndpoint,
|
||||
},
|
||||
}
|
||||
},
|
||||
setParams(curr: RESTSession, { entries }: { entries: HoppRESTParam[] }) {
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
params: entries,
|
||||
},
|
||||
}
|
||||
},
|
||||
addParam(curr: RESTSession, { newParam }: { newParam: HoppRESTParam }) {
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
params: [...curr.request.params, newParam],
|
||||
},
|
||||
}
|
||||
},
|
||||
updateParam(
|
||||
curr: RESTSession,
|
||||
{ index, updatedParam }: { index: number; updatedParam: HoppRESTParam }
|
||||
) {
|
||||
const newParams = curr.request.params.map((param, i) => {
|
||||
if (i === index) return updatedParam
|
||||
else return param
|
||||
})
|
||||
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
params: newParams,
|
||||
},
|
||||
}
|
||||
},
|
||||
deleteParam(curr: RESTSession, { index }: { index: number }) {
|
||||
const newParams = curr.request.params.filter((_x, i) => i !== index)
|
||||
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
params: newParams,
|
||||
},
|
||||
}
|
||||
},
|
||||
deleteAllParams(curr: RESTSession) {
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
params: [],
|
||||
},
|
||||
}
|
||||
},
|
||||
updateMethod(curr: RESTSession, { newMethod }: { newMethod: string }) {
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
method: newMethod,
|
||||
},
|
||||
}
|
||||
},
|
||||
setHeaders(curr: RESTSession, { entries }: { entries: HoppRESTHeader[] }) {
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
headers: entries,
|
||||
},
|
||||
}
|
||||
},
|
||||
addHeader(curr: RESTSession, { entry }: { entry: HoppRESTHeader }) {
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
headers: [...curr.request.headers, entry],
|
||||
},
|
||||
}
|
||||
},
|
||||
updateHeader(
|
||||
curr: RESTSession,
|
||||
{ index, updatedEntry }: { index: number; updatedEntry: HoppRESTHeader }
|
||||
) {
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
headers: curr.request.headers.map((header, i) => {
|
||||
if (i === index) return updatedEntry
|
||||
else return header
|
||||
}),
|
||||
},
|
||||
}
|
||||
},
|
||||
deleteHeader(curr: RESTSession, { index }: { index: number }) {
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
headers: curr.request.headers.filter((_, i) => i !== index),
|
||||
},
|
||||
}
|
||||
},
|
||||
deleteAllHeaders(curr: RESTSession) {
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
headers: [],
|
||||
},
|
||||
}
|
||||
},
|
||||
setAuth(curr: RESTSession, { newAuth }: { newAuth: HoppRESTAuth }) {
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
auth: newAuth,
|
||||
},
|
||||
}
|
||||
},
|
||||
setPreRequestScript(curr: RESTSession, { newScript }: { newScript: string }) {
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
preRequestScript: newScript,
|
||||
},
|
||||
}
|
||||
},
|
||||
setTestScript(curr: RESTSession, { newScript }: { newScript: string }) {
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
testScript: newScript,
|
||||
},
|
||||
}
|
||||
},
|
||||
setContentType(
|
||||
curr: RESTSession,
|
||||
{ newContentType }: { newContentType: ValidContentTypes | null }
|
||||
) {
|
||||
// TODO: persist body evenafter switching content typees
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
body: applyBodyTransition(curr.request.body, newContentType),
|
||||
},
|
||||
}
|
||||
},
|
||||
addFormDataEntry(curr: RESTSession, { entry }: { entry: FormDataKeyValue }) {
|
||||
// Only perform update if the current content-type is formdata
|
||||
if (curr.request.body.contentType !== "multipart/form-data") return {}
|
||||
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
body: <HoppRESTReqBody>{
|
||||
contentType: "multipart/form-data",
|
||||
body: [...curr.request.body.body, entry],
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
deleteFormDataEntry(curr: RESTSession, { index }: { index: number }) {
|
||||
// Only perform update if the current content-type is formdata
|
||||
if (curr.request.body.contentType !== "multipart/form-data") return {}
|
||||
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
body: <HoppRESTReqBody>{
|
||||
contentType: "multipart/form-data",
|
||||
body: curr.request.body.body.filter((_, i) => i !== index),
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
updateFormDataEntry(
|
||||
curr: RESTSession,
|
||||
{ index, entry }: { index: number; entry: FormDataKeyValue }
|
||||
) {
|
||||
// Only perform update if the current content-type is formdata
|
||||
if (curr.request.body.contentType !== "multipart/form-data") return {}
|
||||
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
body: <HoppRESTReqBody>{
|
||||
contentType: "multipart/form-data",
|
||||
body: curr.request.body.body.map((x, i) => (i !== index ? x : entry)),
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
deleteAllFormDataEntries(curr: RESTSession) {
|
||||
// Only perform update if the current content-type is formdata
|
||||
if (curr.request.body.contentType !== "multipart/form-data") return {}
|
||||
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
body: <HoppRESTReqBody>{
|
||||
contentType: "multipart/form-data",
|
||||
body: [],
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
setRequestBody(curr: RESTSession, { newBody }: { newBody: HoppRESTReqBody }) {
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
body: newBody,
|
||||
},
|
||||
}
|
||||
},
|
||||
updateResponse(
|
||||
_curr: RESTSession,
|
||||
{ updatedRes }: { updatedRes: HoppRESTResponse | null }
|
||||
) {
|
||||
return {
|
||||
response: updatedRes,
|
||||
}
|
||||
},
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
clearResponse(_curr: RESTSession) {
|
||||
return {
|
||||
response: null,
|
||||
}
|
||||
},
|
||||
setTestResults(
|
||||
_curr: RESTSession,
|
||||
{ newResults }: { newResults: HoppTestResult | null }
|
||||
) {
|
||||
return {
|
||||
testResults: newResults,
|
||||
}
|
||||
},
|
||||
setSaveContext(
|
||||
_,
|
||||
{ newContext }: { newContext: HoppRequestSaveContext | null }
|
||||
) {
|
||||
return {
|
||||
saveContext: newContext,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const restSessionStore = new DispatchingStore(defaultRESTSession, dispatchers)
|
||||
|
||||
export function getRESTRequest() {
|
||||
return restSessionStore.subject$.value.request
|
||||
}
|
||||
|
||||
export function setRESTRequest(
|
||||
req: HoppRESTRequest,
|
||||
saveContext?: HoppRequestSaveContext | null
|
||||
) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "setRequest",
|
||||
payload: {
|
||||
req,
|
||||
},
|
||||
})
|
||||
|
||||
if (saveContext) setRESTSaveContext(saveContext)
|
||||
}
|
||||
|
||||
export function setRESTSaveContext(saveContext: HoppRequestSaveContext | null) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "setSaveContext",
|
||||
payload: {
|
||||
newContext: saveContext,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function getRESTSaveContext() {
|
||||
return restSessionStore.value.saveContext
|
||||
}
|
||||
|
||||
export function resetRESTRequest() {
|
||||
setRESTRequest(getDefaultRESTRequest())
|
||||
}
|
||||
|
||||
export function setRESTEndpoint(newEndpoint: string) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "setEndpoint",
|
||||
payload: {
|
||||
newEndpoint,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setRESTRequestName(newName: string) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "setRequestName",
|
||||
payload: {
|
||||
newName,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setRESTParams(entries: HoppRESTParam[]) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "setParams",
|
||||
payload: {
|
||||
entries,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function addRESTParam(newParam: HoppRESTParam) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "addParam",
|
||||
payload: {
|
||||
newParam,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function updateRESTParam(index: number, updatedParam: HoppRESTParam) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "updateParam",
|
||||
payload: {
|
||||
updatedParam,
|
||||
index,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteRESTParam(index: number) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "deleteParam",
|
||||
payload: {
|
||||
index,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteAllRESTParams() {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "deleteAllParams",
|
||||
payload: {},
|
||||
})
|
||||
}
|
||||
|
||||
export function updateRESTMethod(newMethod: string) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "updateMethod",
|
||||
payload: {
|
||||
newMethod,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setRESTHeaders(entries: HoppRESTHeader[]) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "setHeaders",
|
||||
payload: {
|
||||
entries,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function addRESTHeader(entry: HoppRESTHeader) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "addHeader",
|
||||
payload: {
|
||||
entry,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function updateRESTHeader(index: number, updatedEntry: HoppRESTHeader) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "updateHeader",
|
||||
payload: {
|
||||
index,
|
||||
updatedEntry,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteRESTHeader(index: number) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "deleteHeader",
|
||||
payload: {
|
||||
index,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteAllRESTHeaders() {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "deleteAllHeaders",
|
||||
payload: {},
|
||||
})
|
||||
}
|
||||
|
||||
export function setRESTAuth(newAuth: HoppRESTAuth) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "setAuth",
|
||||
payload: {
|
||||
newAuth,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setRESTPreRequestScript(newScript: string) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "setPreRequestScript",
|
||||
payload: {
|
||||
newScript,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setRESTTestScript(newScript: string) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "setTestScript",
|
||||
payload: {
|
||||
newScript,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setRESTReqBody(newBody: HoppRESTReqBody | null) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "setRequestBody",
|
||||
payload: {
|
||||
newBody,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function updateRESTResponse(updatedRes: HoppRESTResponse | null) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "updateResponse",
|
||||
payload: {
|
||||
updatedRes,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function clearRESTResponse() {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "clearResponse",
|
||||
payload: {},
|
||||
})
|
||||
}
|
||||
|
||||
export function setRESTTestResults(newResults: HoppTestResult | null) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "setTestResults",
|
||||
payload: {
|
||||
newResults,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function addFormDataEntry(entry: FormDataKeyValue) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "addFormDataEntry",
|
||||
payload: {
|
||||
entry,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteFormDataEntry(index: number) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "deleteFormDataEntry",
|
||||
payload: {
|
||||
index,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function updateFormDataEntry(index: number, entry: FormDataKeyValue) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "updateFormDataEntry",
|
||||
payload: {
|
||||
index,
|
||||
entry,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setRESTContentType(newContentType: ValidContentTypes | null) {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "setContentType",
|
||||
payload: {
|
||||
newContentType,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteAllFormDataEntries() {
|
||||
restSessionStore.dispatch({
|
||||
dispatcher: "deleteAllFormDataEntries",
|
||||
payload: {},
|
||||
})
|
||||
}
|
||||
|
||||
export const restSaveContext$ = restSessionStore.subject$.pipe(
|
||||
pluck("saveContext"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const restRequest$ = restSessionStore.subject$.pipe(
|
||||
pluck("request"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const restRequestName$ = restRequest$.pipe(
|
||||
pluck("name"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const restEndpoint$ = restSessionStore.subject$.pipe(
|
||||
pluck("request", "endpoint"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const restParams$ = restSessionStore.subject$.pipe(
|
||||
pluck("request", "params"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const restActiveParamsCount$ = restParams$.pipe(
|
||||
map(
|
||||
(params) =>
|
||||
params.filter((x) => x.active && (x.key !== "" || x.value !== "")).length
|
||||
)
|
||||
)
|
||||
|
||||
export const restMethod$ = restSessionStore.subject$.pipe(
|
||||
pluck("request", "method"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const restHeaders$ = restSessionStore.subject$.pipe(
|
||||
pluck("request", "headers"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const restActiveHeadersCount$ = restHeaders$.pipe(
|
||||
map(
|
||||
(params) =>
|
||||
params.filter((x) => x.active && (x.key !== "" || x.value !== "")).length
|
||||
)
|
||||
)
|
||||
|
||||
export const restAuth$ = restRequest$.pipe(pluck("auth"))
|
||||
|
||||
export const restPreRequestScript$ = restSessionStore.subject$.pipe(
|
||||
pluck("request", "preRequestScript"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const restContentType$ = restRequest$.pipe(
|
||||
pluck("body", "contentType"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const restTestScript$ = restSessionStore.subject$.pipe(
|
||||
pluck("request", "testScript"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const restReqBody$ = restSessionStore.subject$.pipe(
|
||||
pluck("request", "body"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const restResponse$ = restSessionStore.subject$.pipe(
|
||||
pluck("response"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const completedRESTResponse$ = restResponse$.pipe(
|
||||
filter(
|
||||
(res) =>
|
||||
res !== null &&
|
||||
res.type !== "loading" &&
|
||||
res.type !== "network_fail" &&
|
||||
res.type !== "script_fail"
|
||||
)
|
||||
)
|
||||
|
||||
export const restTestResults$ = restSessionStore.subject$.pipe(
|
||||
pluck("testResults"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
/**
|
||||
* A Vue 3 composable function that gives access to a ref
|
||||
* which is updated to the preRequestScript value in the store.
|
||||
* The ref value is kept in sync with the store and all writes
|
||||
* to the ref are dispatched to the store as `setPreRequestScript`
|
||||
* dispatches.
|
||||
*/
|
||||
export function usePreRequestScript(): Ref<string> {
|
||||
return useStream(
|
||||
restPreRequestScript$,
|
||||
restSessionStore.value.request.preRequestScript,
|
||||
(value) => {
|
||||
setRESTPreRequestScript(value)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A Vue 3 composable function that gives access to a ref
|
||||
* which is updated to the testScript value in the store.
|
||||
* The ref value is kept in sync with the store and all writes
|
||||
* to the ref are dispatched to the store as `setTestScript`
|
||||
* dispatches.
|
||||
*/
|
||||
export function useTestScript(): Ref<string> {
|
||||
return useStream(
|
||||
restTestScript$,
|
||||
restSessionStore.value.request.testScript,
|
||||
(value) => {
|
||||
setRESTTestScript(value)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export function useRESTRequestBody(): Ref<HoppRESTReqBody> {
|
||||
return useStream(
|
||||
restReqBody$,
|
||||
restSessionStore.value.request.body,
|
||||
setRESTReqBody
|
||||
)
|
||||
}
|
||||
|
||||
export function useRESTRequestName(): Ref<string> {
|
||||
return useStream(
|
||||
restRequestName$,
|
||||
restSessionStore.value.request.name,
|
||||
setRESTRequestName
|
||||
)
|
||||
}
|
||||
157
packages/hoppscotch-common/src/newstore/SSESession.ts
Normal file
157
packages/hoppscotch-common/src/newstore/SSESession.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { pluck, distinctUntilChanged } from "rxjs/operators"
|
||||
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
|
||||
import {
|
||||
HoppRealtimeLog,
|
||||
HoppRealtimeLogLine,
|
||||
} from "~/helpers/types/HoppRealtimeLog"
|
||||
import { SSEConnection } from "~/helpers/realtime/SSEConnection"
|
||||
|
||||
type HoppSSERequest = {
|
||||
endpoint: string
|
||||
eventType: string
|
||||
}
|
||||
|
||||
type HoppSSESession = {
|
||||
request: HoppSSERequest
|
||||
log: HoppRealtimeLog
|
||||
socket: SSEConnection
|
||||
}
|
||||
|
||||
const defaultSSERequest: HoppSSERequest = {
|
||||
endpoint: "https://express-eventsource.herokuapp.com/events",
|
||||
eventType: "data",
|
||||
}
|
||||
|
||||
const defaultSSESession: HoppSSESession = {
|
||||
request: defaultSSERequest,
|
||||
socket: new SSEConnection(),
|
||||
log: [],
|
||||
}
|
||||
|
||||
const dispatchers = defineDispatchers({
|
||||
setRequest(
|
||||
_: HoppSSESession,
|
||||
{ newRequest }: { newRequest: HoppSSERequest }
|
||||
) {
|
||||
return {
|
||||
request: newRequest,
|
||||
}
|
||||
},
|
||||
setEndpoint(curr: HoppSSESession, { newEndpoint }: { newEndpoint: string }) {
|
||||
return {
|
||||
request: {
|
||||
eventType: curr.request.eventType,
|
||||
endpoint: newEndpoint,
|
||||
},
|
||||
}
|
||||
},
|
||||
setEventType(curr: HoppSSESession, { newType }: { newType: string }) {
|
||||
return {
|
||||
request: {
|
||||
endpoint: curr.request.endpoint,
|
||||
eventType: newType,
|
||||
},
|
||||
}
|
||||
},
|
||||
setSocket(_: HoppSSESession, { socket }: { socket: SSEConnection }) {
|
||||
return {
|
||||
socket,
|
||||
}
|
||||
},
|
||||
setLog(_: HoppSSESession, { log }: { log: HoppRealtimeLog }) {
|
||||
return {
|
||||
log,
|
||||
}
|
||||
},
|
||||
addLogLine(curr: HoppSSESession, { line }: { line: HoppRealtimeLogLine }) {
|
||||
return {
|
||||
log: [...curr.log, line],
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const SSESessionStore = new DispatchingStore(defaultSSESession, dispatchers)
|
||||
|
||||
export function setSSERequest(newRequest?: HoppSSERequest) {
|
||||
SSESessionStore.dispatch({
|
||||
dispatcher: "setRequest",
|
||||
payload: {
|
||||
newRequest: newRequest ?? defaultSSERequest,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setSSEEndpoint(newEndpoint: string) {
|
||||
SSESessionStore.dispatch({
|
||||
dispatcher: "setEndpoint",
|
||||
payload: {
|
||||
newEndpoint,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setSSEEventType(newType: string) {
|
||||
SSESessionStore.dispatch({
|
||||
dispatcher: "setEventType",
|
||||
payload: {
|
||||
newType,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setSSESocket(socket: SSEConnection) {
|
||||
SSESessionStore.dispatch({
|
||||
dispatcher: "setSocket",
|
||||
payload: {
|
||||
socket,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setSSELog(log: HoppRealtimeLog) {
|
||||
SSESessionStore.dispatch({
|
||||
dispatcher: "setLog",
|
||||
payload: {
|
||||
log,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function addSSELogLine(line: HoppRealtimeLogLine) {
|
||||
SSESessionStore.dispatch({
|
||||
dispatcher: "addLogLine",
|
||||
payload: {
|
||||
line,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const SSERequest$ = SSESessionStore.subject$.pipe(
|
||||
pluck("request"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const SSEEndpoint$ = SSESessionStore.subject$.pipe(
|
||||
pluck("request", "endpoint"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const SSEEventType$ = SSESessionStore.subject$.pipe(
|
||||
pluck("request", "eventType"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const SSEConnectingState$ = SSESessionStore.subject$.pipe(
|
||||
pluck("connectingState"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const SSESocket$ = SSESessionStore.subject$.pipe(
|
||||
pluck("socket"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const SSELog$ = SSESessionStore.subject$.pipe(
|
||||
pluck("log"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
190
packages/hoppscotch-common/src/newstore/SocketIOSession.ts
Normal file
190
packages/hoppscotch-common/src/newstore/SocketIOSession.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
import { pluck, distinctUntilChanged } from "rxjs/operators"
|
||||
import { Socket as SocketV2 } from "socket.io-client-v2"
|
||||
import { Socket as SocketV3 } from "socket.io-client-v3"
|
||||
import { Socket as SocketV4 } from "socket.io-client-v4"
|
||||
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
|
||||
import {
|
||||
HoppRealtimeLog,
|
||||
HoppRealtimeLogLine,
|
||||
} from "~/helpers/types/HoppRealtimeLog"
|
||||
|
||||
type SocketIO = SocketV2 | SocketV3 | SocketV4
|
||||
|
||||
export type SIOClientVersion = "v4" | "v3" | "v2"
|
||||
|
||||
type HoppSIORequest = {
|
||||
endpoint: string
|
||||
path: string
|
||||
version: SIOClientVersion
|
||||
}
|
||||
|
||||
type HoppSIOSession = {
|
||||
request: HoppSIORequest
|
||||
log: HoppRealtimeLog
|
||||
socket: SocketIO | null
|
||||
}
|
||||
|
||||
const defaultSIORequest: HoppSIORequest = {
|
||||
endpoint: "wss://echo-socketio.hoppscotch.io",
|
||||
path: "/socket.io",
|
||||
version: "v4",
|
||||
}
|
||||
|
||||
const defaultSIOSession: HoppSIOSession = {
|
||||
request: defaultSIORequest,
|
||||
socket: null,
|
||||
log: [],
|
||||
}
|
||||
|
||||
const dispatchers = defineDispatchers({
|
||||
setRequest(
|
||||
_: HoppSIOSession,
|
||||
{ newRequest }: { newRequest: HoppSIORequest }
|
||||
) {
|
||||
return {
|
||||
request: newRequest,
|
||||
}
|
||||
},
|
||||
setEndpoint(curr: HoppSIOSession, { newEndpoint }: { newEndpoint: string }) {
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
endpoint: newEndpoint,
|
||||
},
|
||||
}
|
||||
},
|
||||
setPath(curr: HoppSIOSession, { newPath }: { newPath: string }) {
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
path: newPath,
|
||||
},
|
||||
}
|
||||
},
|
||||
setVersion(
|
||||
curr: HoppSIOSession,
|
||||
{ newVersion }: { newVersion: SIOClientVersion }
|
||||
) {
|
||||
return {
|
||||
request: {
|
||||
...curr.request,
|
||||
version: newVersion,
|
||||
},
|
||||
}
|
||||
},
|
||||
setSocket(_: HoppSIOSession, { socket }: { socket: SocketIO }) {
|
||||
return {
|
||||
socket,
|
||||
}
|
||||
},
|
||||
setLog(_: HoppSIOSession, { log }: { log: HoppRealtimeLog }) {
|
||||
return {
|
||||
log,
|
||||
}
|
||||
},
|
||||
addLogLine(curr: HoppSIOSession, { line }: { line: HoppRealtimeLogLine }) {
|
||||
return {
|
||||
log: [...curr.log, line],
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const SIOSessionStore = new DispatchingStore(defaultSIOSession, dispatchers)
|
||||
|
||||
export function setSIORequest(newRequest?: HoppSIORequest) {
|
||||
SIOSessionStore.dispatch({
|
||||
dispatcher: "setRequest",
|
||||
payload: {
|
||||
newRequest: newRequest ?? defaultSIORequest,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setSIOEndpoint(newEndpoint: string) {
|
||||
SIOSessionStore.dispatch({
|
||||
dispatcher: "setEndpoint",
|
||||
payload: {
|
||||
newEndpoint,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setSIOVersion(newVersion: string) {
|
||||
SIOSessionStore.dispatch({
|
||||
dispatcher: "setVersion",
|
||||
payload: {
|
||||
newVersion,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setSIOPath(newPath: string) {
|
||||
SIOSessionStore.dispatch({
|
||||
dispatcher: "setPath",
|
||||
payload: {
|
||||
newPath,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setSIOSocket(socket: SocketIO) {
|
||||
SIOSessionStore.dispatch({
|
||||
dispatcher: "setSocket",
|
||||
payload: {
|
||||
socket,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setSIOLog(log: HoppRealtimeLog) {
|
||||
SIOSessionStore.dispatch({
|
||||
dispatcher: "setLog",
|
||||
payload: {
|
||||
log,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function addSIOLogLine(line: HoppRealtimeLogLine) {
|
||||
SIOSessionStore.dispatch({
|
||||
dispatcher: "addLogLine",
|
||||
payload: {
|
||||
line,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const SIORequest$ = SIOSessionStore.subject$.pipe(
|
||||
pluck("request"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const SIOEndpoint$ = SIOSessionStore.subject$.pipe(
|
||||
pluck("request", "endpoint"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const SIOVersion$ = SIOSessionStore.subject$.pipe(
|
||||
pluck("request", "version"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const SIOPath$ = SIOSessionStore.subject$.pipe(
|
||||
pluck("request", "path"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const SIOConnectionState$ = SIOSessionStore.subject$.pipe(
|
||||
pluck("connectionState"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const SIOSocket$ = SIOSessionStore.subject$.pipe(
|
||||
pluck("socket"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const SIOLog$ = SIOSessionStore.subject$.pipe(
|
||||
pluck("log"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
245
packages/hoppscotch-common/src/newstore/WebSocketSession.ts
Normal file
245
packages/hoppscotch-common/src/newstore/WebSocketSession.ts
Normal file
@@ -0,0 +1,245 @@
|
||||
import { pluck, distinctUntilChanged } from "rxjs/operators"
|
||||
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
|
||||
import {
|
||||
HoppRealtimeLog,
|
||||
HoppRealtimeLogLine,
|
||||
} from "~/helpers/types/HoppRealtimeLog"
|
||||
import { WSConnection } from "~/helpers/realtime/WSConnection"
|
||||
|
||||
export type HoppWSProtocol = {
|
||||
value: string
|
||||
active: boolean
|
||||
}
|
||||
|
||||
type HoppWSRequest = {
|
||||
endpoint: string
|
||||
protocols: HoppWSProtocol[]
|
||||
}
|
||||
|
||||
export type HoppWSSession = {
|
||||
request: HoppWSRequest
|
||||
log: HoppRealtimeLog
|
||||
socket: WSConnection
|
||||
}
|
||||
|
||||
const defaultWSRequest: HoppWSRequest = {
|
||||
endpoint: "wss://echo-websocket.hoppscotch.io",
|
||||
protocols: [],
|
||||
}
|
||||
|
||||
const defaultWSSession: HoppWSSession = {
|
||||
request: defaultWSRequest,
|
||||
socket: new WSConnection(),
|
||||
log: [],
|
||||
}
|
||||
|
||||
const dispatchers = defineDispatchers({
|
||||
setRequest(_: HoppWSSession, { newRequest }: { newRequest: HoppWSRequest }) {
|
||||
return {
|
||||
request: newRequest,
|
||||
}
|
||||
},
|
||||
setEndpoint(curr: HoppWSSession, { newEndpoint }: { newEndpoint: string }) {
|
||||
return {
|
||||
request: {
|
||||
protocols: curr.request.protocols,
|
||||
endpoint: newEndpoint,
|
||||
},
|
||||
}
|
||||
},
|
||||
setProtocols(
|
||||
curr: HoppWSSession,
|
||||
{ protocols }: { protocols: HoppWSProtocol[] }
|
||||
) {
|
||||
return {
|
||||
request: {
|
||||
protocols,
|
||||
endpoint: curr.request.endpoint,
|
||||
},
|
||||
}
|
||||
},
|
||||
addProtocol(curr: HoppWSSession, { protocol }: { protocol: HoppWSProtocol }) {
|
||||
return {
|
||||
request: {
|
||||
endpoint: curr.request.endpoint,
|
||||
protocols: [...curr.request.protocols, protocol],
|
||||
},
|
||||
}
|
||||
},
|
||||
deleteProtocol(curr: HoppWSSession, { index }: { index: number }) {
|
||||
return {
|
||||
request: {
|
||||
endpoint: curr.request.endpoint,
|
||||
protocols: curr.request.protocols.filter((_, idx) => index !== idx),
|
||||
},
|
||||
}
|
||||
},
|
||||
deleteAllProtocols(curr: HoppWSSession) {
|
||||
return {
|
||||
request: {
|
||||
endpoint: curr.request.endpoint,
|
||||
protocols: [],
|
||||
},
|
||||
}
|
||||
},
|
||||
updateProtocol(
|
||||
curr: HoppWSSession,
|
||||
{
|
||||
index,
|
||||
updatedProtocol,
|
||||
}: { index: number; updatedProtocol: HoppWSProtocol }
|
||||
) {
|
||||
return {
|
||||
request: {
|
||||
endpoint: curr.request.endpoint,
|
||||
protocols: curr.request.protocols.map((proto, idx) => {
|
||||
return index === idx ? updatedProtocol : proto
|
||||
}),
|
||||
},
|
||||
}
|
||||
},
|
||||
setSocket(_: HoppWSSession, { socket }: { socket: WSConnection }) {
|
||||
return {
|
||||
socket,
|
||||
}
|
||||
},
|
||||
setLog(_: HoppWSSession, { log }: { log: HoppRealtimeLog }) {
|
||||
return {
|
||||
log,
|
||||
}
|
||||
},
|
||||
addLogLine(curr: HoppWSSession, { line }: { line: HoppRealtimeLogLine }) {
|
||||
return {
|
||||
log: [...curr.log, line],
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const WSSessionStore = new DispatchingStore(defaultWSSession, dispatchers)
|
||||
|
||||
export function setWSRequest(newRequest?: HoppWSRequest) {
|
||||
WSSessionStore.dispatch({
|
||||
dispatcher: "setRequest",
|
||||
payload: {
|
||||
newRequest: newRequest ?? defaultWSRequest,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setWSEndpoint(newEndpoint: string) {
|
||||
WSSessionStore.dispatch({
|
||||
dispatcher: "setEndpoint",
|
||||
payload: {
|
||||
newEndpoint,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setWSProtocols(protocols: HoppWSProtocol[]) {
|
||||
WSSessionStore.dispatch({
|
||||
dispatcher: "setProtocols",
|
||||
payload: {
|
||||
protocols,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function addWSProtocol(protocol: HoppWSProtocol) {
|
||||
WSSessionStore.dispatch({
|
||||
dispatcher: "addProtocol",
|
||||
payload: {
|
||||
protocol,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteWSProtocol(index: number) {
|
||||
WSSessionStore.dispatch({
|
||||
dispatcher: "deleteProtocol",
|
||||
payload: {
|
||||
index,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteAllWSProtocols() {
|
||||
WSSessionStore.dispatch({
|
||||
dispatcher: "deleteAllProtocols",
|
||||
payload: {},
|
||||
})
|
||||
}
|
||||
|
||||
export function updateWSProtocol(
|
||||
index: number,
|
||||
updatedProtocol: HoppWSProtocol
|
||||
) {
|
||||
WSSessionStore.dispatch({
|
||||
dispatcher: "updateProtocol",
|
||||
payload: {
|
||||
index,
|
||||
updatedProtocol,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setWSSocket(socket: WSConnection) {
|
||||
WSSessionStore.dispatch({
|
||||
dispatcher: "setSocket",
|
||||
payload: {
|
||||
socket,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setWSLog(log: HoppRealtimeLog) {
|
||||
WSSessionStore.dispatch({
|
||||
dispatcher: "setLog",
|
||||
payload: {
|
||||
log,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function addWSLogLine(line: HoppRealtimeLogLine) {
|
||||
WSSessionStore.dispatch({
|
||||
dispatcher: "addLogLine",
|
||||
payload: {
|
||||
line,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const WSRequest$ = WSSessionStore.subject$.pipe(
|
||||
pluck("request"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const WSEndpoint$ = WSSessionStore.subject$.pipe(
|
||||
pluck("request", "endpoint"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const WSProtocols$ = WSSessionStore.subject$.pipe(
|
||||
pluck("request", "protocols"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const WSConnectingState$ = WSSessionStore.subject$.pipe(
|
||||
pluck("connectingState"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const WSConnectionState$ = WSSessionStore.subject$.pipe(
|
||||
pluck("connectionState"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const WSSocket$ = WSSessionStore.subject$.pipe(
|
||||
pluck("socket"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const WSLog$ = WSSessionStore.subject$.pipe(
|
||||
pluck("log"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
@@ -0,0 +1,193 @@
|
||||
import { BehaviorSubject, Subject } from "rxjs"
|
||||
import { isEqual } from "lodash-es"
|
||||
import DispatchingStore from "~/newstore/DispatchingStore"
|
||||
|
||||
describe("DispatchingStore", () => {
|
||||
test("'subject$' property properly returns an BehaviorSubject", () => {
|
||||
const store = new DispatchingStore({}, {})
|
||||
|
||||
expect(store.subject$ instanceof BehaviorSubject).toEqual(true)
|
||||
})
|
||||
|
||||
test("'value' property properly returns the current state value", () => {
|
||||
const store = new DispatchingStore({}, {})
|
||||
|
||||
expect(store.value).toEqual({})
|
||||
})
|
||||
|
||||
test("'dispatches$' property properly returns a Subject", () => {
|
||||
const store = new DispatchingStore({}, {})
|
||||
|
||||
expect(store.dispatches$ instanceof Subject).toEqual(true)
|
||||
})
|
||||
|
||||
test("dispatch with invalid dispatcher are thrown", () => {
|
||||
const store = new DispatchingStore({}, {})
|
||||
|
||||
expect(() => {
|
||||
store.dispatch({
|
||||
dispatcher: "non-existent",
|
||||
payload: {},
|
||||
})
|
||||
}).toThrow()
|
||||
})
|
||||
|
||||
test("valid dispatcher calls run without throwing", () => {
|
||||
const store = new DispatchingStore(
|
||||
{},
|
||||
{
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
testDispatcher(_currentValue, _payload) {
|
||||
// Nothing here
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
expect(() => {
|
||||
store.dispatch({
|
||||
dispatcher: "testDispatcher",
|
||||
payload: {},
|
||||
})
|
||||
}).not.toThrow()
|
||||
})
|
||||
|
||||
test("only correct dispatcher method is ran", () => {
|
||||
const dispatchFn = jest.fn().mockReturnValue({})
|
||||
const dontCallDispatchFn = jest.fn().mockReturnValue({})
|
||||
|
||||
const store = new DispatchingStore(
|
||||
{},
|
||||
{
|
||||
testDispatcher: dispatchFn,
|
||||
dontCallDispatcher: dontCallDispatchFn,
|
||||
}
|
||||
)
|
||||
|
||||
store.dispatch({
|
||||
dispatcher: "testDispatcher",
|
||||
payload: {},
|
||||
})
|
||||
|
||||
expect(dispatchFn).toHaveBeenCalledTimes(1)
|
||||
expect(dontCallDispatchFn).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
test("passes current value and the payload to the dispatcher", () => {
|
||||
const testInitValue = { name: "bob" }
|
||||
const testPayload = { name: "alice" }
|
||||
|
||||
const testDispatchFn = jest.fn().mockReturnValue({})
|
||||
|
||||
const store = new DispatchingStore(testInitValue, {
|
||||
testDispatcher: testDispatchFn,
|
||||
})
|
||||
|
||||
store.dispatch({
|
||||
dispatcher: "testDispatcher",
|
||||
payload: testPayload,
|
||||
})
|
||||
|
||||
expect(testDispatchFn).toHaveBeenCalledWith(testInitValue, testPayload)
|
||||
})
|
||||
|
||||
test("dispatcher returns are used to update the store correctly", () => {
|
||||
const testInitValue = { name: "bob" }
|
||||
const testDispatchReturnVal = { name: "alice" }
|
||||
|
||||
const testDispatchFn = jest.fn().mockReturnValue(testDispatchReturnVal)
|
||||
|
||||
const store = new DispatchingStore(testInitValue, {
|
||||
testDispatcher: testDispatchFn,
|
||||
})
|
||||
|
||||
store.dispatch({
|
||||
dispatcher: "testDispatcher",
|
||||
payload: {}, // Payload doesn't matter because the function is mocked
|
||||
})
|
||||
|
||||
expect(store.value).toEqual(testDispatchReturnVal)
|
||||
})
|
||||
|
||||
test("dispatching patches in new values if not existing on the store", () => {
|
||||
const testInitValue = { name: "bob" }
|
||||
const testDispatchReturnVal = { age: 25 }
|
||||
|
||||
const testDispatchFn = jest.fn().mockReturnValue(testDispatchReturnVal)
|
||||
|
||||
const store = new DispatchingStore(testInitValue, {
|
||||
testDispatcher: testDispatchFn,
|
||||
})
|
||||
|
||||
store.dispatch({
|
||||
dispatcher: "testDispatcher",
|
||||
payload: {},
|
||||
})
|
||||
|
||||
expect(store.value).toEqual({
|
||||
name: "bob",
|
||||
age: 25,
|
||||
})
|
||||
})
|
||||
|
||||
test("emits the current store value to the new subscribers", (done) => {
|
||||
const testInitValue = { name: "bob" }
|
||||
|
||||
const testDispatchFn = jest.fn().mockReturnValue({})
|
||||
|
||||
const store = new DispatchingStore(testInitValue, {
|
||||
testDispatcher: testDispatchFn,
|
||||
})
|
||||
|
||||
store.subject$.subscribe((value) => {
|
||||
if (value === testInitValue) {
|
||||
done()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
test("emits the dispatched store value to the subscribers", (done) => {
|
||||
const testInitValue = { name: "bob" }
|
||||
const testDispatchReturnVal = { age: 25 }
|
||||
|
||||
const testDispatchFn = jest.fn().mockReturnValue(testDispatchReturnVal)
|
||||
|
||||
const store = new DispatchingStore(testInitValue, {
|
||||
testDispatcher: testDispatchFn,
|
||||
})
|
||||
|
||||
store.subject$.subscribe((value) => {
|
||||
if (isEqual(value, { name: "bob", age: 25 })) {
|
||||
done()
|
||||
}
|
||||
})
|
||||
|
||||
store.dispatch({
|
||||
dispatcher: "testDispatcher",
|
||||
payload: {},
|
||||
})
|
||||
})
|
||||
|
||||
test("dispatching emits the new dispatch requests to the subscribers", () => {
|
||||
const testInitValue = { name: "bob" }
|
||||
const testPayload = { age: 25 }
|
||||
|
||||
const testDispatchFn = jest.fn().mockReturnValue({})
|
||||
|
||||
const store = new DispatchingStore(testInitValue, {
|
||||
testDispatcher: testDispatchFn,
|
||||
})
|
||||
|
||||
store.dispatches$.subscribe((value) => {
|
||||
if (
|
||||
isEqual(value, { dispatcher: "testDispatcher", payload: testPayload })
|
||||
) {
|
||||
done()
|
||||
}
|
||||
})
|
||||
|
||||
store.dispatch({
|
||||
dispatcher: "testDispatcher",
|
||||
payload: {},
|
||||
})
|
||||
})
|
||||
})
|
||||
889
packages/hoppscotch-common/src/newstore/collections.ts
Normal file
889
packages/hoppscotch-common/src/newstore/collections.ts
Normal file
@@ -0,0 +1,889 @@
|
||||
import { pluck } from "rxjs/operators"
|
||||
import {
|
||||
HoppGQLRequest,
|
||||
HoppRESTRequest,
|
||||
HoppCollection,
|
||||
makeCollection,
|
||||
} from "@hoppscotch/data"
|
||||
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
|
||||
import { getRESTSaveContext, setRESTSaveContext } from "./RESTSession"
|
||||
|
||||
const defaultRESTCollectionState = {
|
||||
state: [
|
||||
makeCollection<HoppRESTRequest>({
|
||||
name: "My Collection",
|
||||
folders: [],
|
||||
requests: [],
|
||||
}),
|
||||
],
|
||||
}
|
||||
|
||||
const defaultGraphqlCollectionState = {
|
||||
state: [
|
||||
makeCollection<HoppGQLRequest>({
|
||||
name: "My GraphQL Collection",
|
||||
folders: [],
|
||||
requests: [],
|
||||
}),
|
||||
],
|
||||
}
|
||||
|
||||
type RESTCollectionStoreType = typeof defaultRESTCollectionState
|
||||
type GraphqlCollectionStoreType = typeof defaultGraphqlCollectionState
|
||||
|
||||
function navigateToFolderWithIndexPath(
|
||||
collections: HoppCollection<HoppRESTRequest | HoppGQLRequest>[],
|
||||
indexPaths: number[]
|
||||
) {
|
||||
if (indexPaths.length === 0) return null
|
||||
|
||||
let target = collections[indexPaths.shift() as number]
|
||||
|
||||
while (indexPaths.length > 0)
|
||||
target = target.folders[indexPaths.shift() as number]
|
||||
|
||||
return target !== undefined ? target : null
|
||||
}
|
||||
|
||||
const restCollectionDispatchers = defineDispatchers({
|
||||
setCollections(
|
||||
_: RESTCollectionStoreType,
|
||||
{ entries }: { entries: HoppCollection<HoppRESTRequest>[] }
|
||||
) {
|
||||
return {
|
||||
state: entries,
|
||||
}
|
||||
},
|
||||
|
||||
appendCollections(
|
||||
{ state }: RESTCollectionStoreType,
|
||||
{ entries }: { entries: HoppCollection<HoppRESTRequest>[] }
|
||||
) {
|
||||
return {
|
||||
state: [...state, ...entries],
|
||||
}
|
||||
},
|
||||
|
||||
addCollection(
|
||||
{ state }: RESTCollectionStoreType,
|
||||
{ collection }: { collection: HoppCollection<any> }
|
||||
) {
|
||||
return {
|
||||
state: [...state, collection],
|
||||
}
|
||||
},
|
||||
|
||||
removeCollection(
|
||||
{ state }: RESTCollectionStoreType,
|
||||
{ collectionIndex }: { collectionIndex: number }
|
||||
) {
|
||||
return {
|
||||
state: (state as any).filter(
|
||||
(_: any, i: number) => i !== collectionIndex
|
||||
),
|
||||
}
|
||||
},
|
||||
|
||||
editCollection(
|
||||
{ state }: RESTCollectionStoreType,
|
||||
{
|
||||
collectionIndex,
|
||||
collection,
|
||||
}: { collectionIndex: number; collection: HoppCollection<any> }
|
||||
) {
|
||||
return {
|
||||
state: state.map((col, index) =>
|
||||
index === collectionIndex ? collection : col
|
||||
),
|
||||
}
|
||||
},
|
||||
|
||||
addFolder(
|
||||
{ state }: RESTCollectionStoreType,
|
||||
{ name, path }: { name: string; path: string }
|
||||
) {
|
||||
const newFolder: HoppCollection<HoppRESTRequest> = makeCollection({
|
||||
name,
|
||||
folders: [],
|
||||
requests: [],
|
||||
})
|
||||
|
||||
const newState = state
|
||||
const indexPaths = path.split("/").map((x) => parseInt(x))
|
||||
|
||||
const target = navigateToFolderWithIndexPath(newState, indexPaths)
|
||||
|
||||
if (target === null) {
|
||||
console.log(`Could not parse path '${path}'. Ignoring add folder request`)
|
||||
return {}
|
||||
}
|
||||
|
||||
target.folders.push(newFolder)
|
||||
|
||||
return {
|
||||
state: newState,
|
||||
}
|
||||
},
|
||||
|
||||
editFolder(
|
||||
{ state }: RESTCollectionStoreType,
|
||||
{ path, folder }: { path: string; folder: string }
|
||||
) {
|
||||
const newState = state
|
||||
|
||||
const indexPaths = path.split("/").map((x) => parseInt(x))
|
||||
|
||||
const target = navigateToFolderWithIndexPath(newState, indexPaths)
|
||||
|
||||
if (target === null) {
|
||||
console.log(
|
||||
`Could not parse path '${path}'. Ignoring edit folder request`
|
||||
)
|
||||
return {}
|
||||
}
|
||||
|
||||
Object.assign(target, folder)
|
||||
|
||||
return {
|
||||
state: newState,
|
||||
}
|
||||
},
|
||||
|
||||
removeFolder({ state }: RESTCollectionStoreType, { path }: { path: string }) {
|
||||
const newState = state
|
||||
|
||||
const indexPaths = path.split("/").map((x) => parseInt(x))
|
||||
if (indexPaths.length === 0) {
|
||||
console.log(
|
||||
"Given path too short. If this is a collection, use removeCollection dispatcher instead. Skipping request."
|
||||
)
|
||||
return {}
|
||||
}
|
||||
// We get the index path to the folder itself,
|
||||
// we have to find the folder containing the target folder,
|
||||
// so we pop the last path index
|
||||
const folderIndex = indexPaths.pop() as number
|
||||
|
||||
const containingFolder = navigateToFolderWithIndexPath(newState, indexPaths)
|
||||
|
||||
if (containingFolder === null) {
|
||||
console.log(
|
||||
`Could not resolve path '${path}'. Skipping removeFolder dispatch.`
|
||||
)
|
||||
return {}
|
||||
}
|
||||
|
||||
containingFolder.folders.splice(folderIndex, 1)
|
||||
|
||||
return {
|
||||
state: newState,
|
||||
}
|
||||
},
|
||||
|
||||
editRequest(
|
||||
{ state }: RESTCollectionStoreType,
|
||||
{
|
||||
path,
|
||||
requestIndex,
|
||||
requestNew,
|
||||
}: { path: string; requestIndex: number; requestNew: any }
|
||||
) {
|
||||
const newState = state
|
||||
|
||||
const indexPaths = path.split("/").map((x) => parseInt(x))
|
||||
|
||||
const targetLocation = navigateToFolderWithIndexPath(newState, indexPaths)
|
||||
|
||||
if (targetLocation === null) {
|
||||
console.log(
|
||||
`Could not resolve path '${path}'. Ignoring editRequest dispatch.`
|
||||
)
|
||||
return {}
|
||||
}
|
||||
|
||||
targetLocation.requests = targetLocation.requests.map((req, index) =>
|
||||
index !== requestIndex ? req : requestNew
|
||||
)
|
||||
|
||||
return {
|
||||
state: newState,
|
||||
}
|
||||
},
|
||||
|
||||
saveRequestAs(
|
||||
{ state }: RESTCollectionStoreType,
|
||||
{ path, request }: { path: string; request: any }
|
||||
) {
|
||||
const newState = state
|
||||
|
||||
const indexPaths = path.split("/").map((x) => parseInt(x))
|
||||
|
||||
const targetLocation = navigateToFolderWithIndexPath(newState, indexPaths)
|
||||
|
||||
if (targetLocation === null) {
|
||||
console.log(
|
||||
`Could not resolve path '${path}'. Ignoring saveRequestAs dispatch.`
|
||||
)
|
||||
return {}
|
||||
}
|
||||
|
||||
targetLocation.requests.push(request)
|
||||
|
||||
return {
|
||||
state: newState,
|
||||
}
|
||||
},
|
||||
|
||||
removeRequest(
|
||||
{ state }: RESTCollectionStoreType,
|
||||
{ path, requestIndex }: { path: string; requestIndex: number }
|
||||
) {
|
||||
const newState = state
|
||||
|
||||
const indexPaths = path.split("/").map((x) => parseInt(x))
|
||||
|
||||
const targetLocation = navigateToFolderWithIndexPath(newState, indexPaths)
|
||||
|
||||
if (targetLocation === null) {
|
||||
console.log(
|
||||
`Could not resolve path '${path}'. Ignoring removeRequest dispatch.`
|
||||
)
|
||||
return {}
|
||||
}
|
||||
|
||||
targetLocation.requests.splice(requestIndex, 1)
|
||||
|
||||
// If the save context is set and is set to the same source, we invalidate it
|
||||
const saveCtx = getRESTSaveContext()
|
||||
if (
|
||||
saveCtx?.originLocation === "user-collection" &&
|
||||
saveCtx.folderPath === path &&
|
||||
saveCtx.requestIndex === requestIndex
|
||||
) {
|
||||
setRESTSaveContext(null)
|
||||
}
|
||||
|
||||
return {
|
||||
state: newState,
|
||||
}
|
||||
},
|
||||
|
||||
moveRequest(
|
||||
{ state }: RESTCollectionStoreType,
|
||||
{
|
||||
path,
|
||||
requestIndex,
|
||||
destinationPath,
|
||||
}: { path: string; requestIndex: number; destinationPath: string }
|
||||
) {
|
||||
const newState = state
|
||||
|
||||
const indexPaths = path.split("/").map((x) => parseInt(x))
|
||||
|
||||
const targetLocation = navigateToFolderWithIndexPath(newState, indexPaths)
|
||||
|
||||
if (targetLocation === null) {
|
||||
console.log(
|
||||
`Could not resolve source path '${path}'. Skipping moveRequest dispatch.`
|
||||
)
|
||||
return {}
|
||||
}
|
||||
|
||||
const req = targetLocation.requests[requestIndex]
|
||||
|
||||
const destIndexPaths = destinationPath.split("/").map((x) => parseInt(x))
|
||||
|
||||
const destLocation = navigateToFolderWithIndexPath(newState, destIndexPaths)
|
||||
|
||||
if (destLocation === null) {
|
||||
console.log(
|
||||
`Could not resolve destination path '${destinationPath}'. Skipping moveRequest dispatch.`
|
||||
)
|
||||
return {}
|
||||
}
|
||||
|
||||
destLocation.requests.push(req)
|
||||
targetLocation.requests.splice(requestIndex, 1)
|
||||
|
||||
return {
|
||||
state: newState,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const gqlCollectionDispatchers = defineDispatchers({
|
||||
setCollections(
|
||||
_: GraphqlCollectionStoreType,
|
||||
{ entries }: { entries: HoppCollection<any>[] }
|
||||
) {
|
||||
return {
|
||||
state: entries,
|
||||
}
|
||||
},
|
||||
|
||||
appendCollections(
|
||||
{ state }: GraphqlCollectionStoreType,
|
||||
{ entries }: { entries: HoppCollection<any>[] }
|
||||
) {
|
||||
return {
|
||||
state: [...state, ...entries],
|
||||
}
|
||||
},
|
||||
|
||||
addCollection(
|
||||
{ state }: GraphqlCollectionStoreType,
|
||||
{ collection }: { collection: HoppCollection<any> }
|
||||
) {
|
||||
return {
|
||||
state: [...state, collection],
|
||||
}
|
||||
},
|
||||
|
||||
removeCollection(
|
||||
{ state }: GraphqlCollectionStoreType,
|
||||
{ collectionIndex }: { collectionIndex: number }
|
||||
) {
|
||||
return {
|
||||
state: (state as any).filter(
|
||||
(_: any, i: number) => i !== collectionIndex
|
||||
),
|
||||
}
|
||||
},
|
||||
|
||||
editCollection(
|
||||
{ state }: GraphqlCollectionStoreType,
|
||||
{
|
||||
collectionIndex,
|
||||
collection,
|
||||
}: { collectionIndex: number; collection: HoppCollection<any> }
|
||||
) {
|
||||
return {
|
||||
state: state.map((col, index) =>
|
||||
index === collectionIndex ? collection : col
|
||||
),
|
||||
}
|
||||
},
|
||||
|
||||
addFolder(
|
||||
{ state }: GraphqlCollectionStoreType,
|
||||
{ name, path }: { name: string; path: string }
|
||||
) {
|
||||
const newFolder: HoppCollection<HoppGQLRequest> = makeCollection({
|
||||
name,
|
||||
folders: [],
|
||||
requests: [],
|
||||
})
|
||||
|
||||
const newState = state
|
||||
const indexPaths = path.split("/").map((x) => parseInt(x))
|
||||
|
||||
const target = navigateToFolderWithIndexPath(newState, indexPaths)
|
||||
|
||||
if (target === null) {
|
||||
console.log(`Could not parse path '${path}'. Ignoring add folder request`)
|
||||
return {}
|
||||
}
|
||||
|
||||
target.folders.push(newFolder)
|
||||
|
||||
return {
|
||||
state: newState,
|
||||
}
|
||||
},
|
||||
|
||||
editFolder(
|
||||
{ state }: GraphqlCollectionStoreType,
|
||||
{ path, folder }: { path: string; folder: string }
|
||||
) {
|
||||
const newState = state
|
||||
|
||||
const indexPaths = path.split("/").map((x) => parseInt(x))
|
||||
|
||||
const target = navigateToFolderWithIndexPath(newState, indexPaths)
|
||||
|
||||
if (target === null) {
|
||||
console.log(
|
||||
`Could not parse path '${path}'. Ignoring edit folder request`
|
||||
)
|
||||
return {}
|
||||
}
|
||||
|
||||
Object.assign(target, folder)
|
||||
|
||||
return {
|
||||
state: newState,
|
||||
}
|
||||
},
|
||||
|
||||
removeFolder(
|
||||
{ state }: GraphqlCollectionStoreType,
|
||||
{ path }: { path: string }
|
||||
) {
|
||||
const newState = state
|
||||
|
||||
const indexPaths = path.split("/").map((x) => parseInt(x))
|
||||
if (indexPaths.length === 0) {
|
||||
console.log(
|
||||
"Given path too short. If this is a collection, use removeCollection dispatcher instead. Skipping request."
|
||||
)
|
||||
return {}
|
||||
}
|
||||
// We get the index path to the folder itself,
|
||||
// we have to find the folder containing the target folder,
|
||||
// so we pop the last path index
|
||||
const folderIndex = indexPaths.pop() as number
|
||||
|
||||
const containingFolder = navigateToFolderWithIndexPath(newState, indexPaths)
|
||||
|
||||
if (containingFolder === null) {
|
||||
console.log(
|
||||
`Could not resolve path '${path}'. Skipping removeFolder dispatch.`
|
||||
)
|
||||
return {}
|
||||
}
|
||||
|
||||
containingFolder.folders.splice(folderIndex, 1)
|
||||
|
||||
return {
|
||||
state: newState,
|
||||
}
|
||||
},
|
||||
|
||||
editRequest(
|
||||
{ state }: GraphqlCollectionStoreType,
|
||||
{
|
||||
path,
|
||||
requestIndex,
|
||||
requestNew,
|
||||
}: { path: string; requestIndex: number; requestNew: any }
|
||||
) {
|
||||
const newState = state
|
||||
|
||||
const indexPaths = path.split("/").map((x) => parseInt(x))
|
||||
|
||||
const targetLocation = navigateToFolderWithIndexPath(newState, indexPaths)
|
||||
|
||||
if (targetLocation === null) {
|
||||
console.log(
|
||||
`Could not resolve path '${path}'. Ignoring editRequest dispatch.`
|
||||
)
|
||||
return {}
|
||||
}
|
||||
|
||||
targetLocation.requests = targetLocation.requests.map((req, index) =>
|
||||
index !== requestIndex ? req : requestNew
|
||||
)
|
||||
|
||||
return {
|
||||
state: newState,
|
||||
}
|
||||
},
|
||||
|
||||
saveRequestAs(
|
||||
{ state }: GraphqlCollectionStoreType,
|
||||
{ path, request }: { path: string; request: any }
|
||||
) {
|
||||
const newState = state
|
||||
|
||||
const indexPaths = path.split("/").map((x) => parseInt(x))
|
||||
|
||||
const targetLocation = navigateToFolderWithIndexPath(newState, indexPaths)
|
||||
|
||||
if (targetLocation === null) {
|
||||
console.log(
|
||||
`Could not resolve path '${path}'. Ignoring saveRequestAs dispatch.`
|
||||
)
|
||||
return {}
|
||||
}
|
||||
|
||||
targetLocation.requests.push(request)
|
||||
|
||||
return {
|
||||
state: newState,
|
||||
}
|
||||
},
|
||||
|
||||
removeRequest(
|
||||
{ state }: GraphqlCollectionStoreType,
|
||||
{ path, requestIndex }: { path: string; requestIndex: number }
|
||||
) {
|
||||
const newState = state
|
||||
|
||||
const indexPaths = path.split("/").map((x) => parseInt(x))
|
||||
|
||||
const targetLocation = navigateToFolderWithIndexPath(newState, indexPaths)
|
||||
|
||||
if (targetLocation === null) {
|
||||
console.log(
|
||||
`Could not resolve path '${path}'. Ignoring removeRequest dispatch.`
|
||||
)
|
||||
return {}
|
||||
}
|
||||
|
||||
targetLocation.requests.splice(requestIndex, 1)
|
||||
|
||||
// If the save context is set and is set to the same source, we invalidate it
|
||||
const saveCtx = getRESTSaveContext()
|
||||
if (
|
||||
saveCtx?.originLocation === "user-collection" &&
|
||||
saveCtx.folderPath === path &&
|
||||
saveCtx.requestIndex === requestIndex
|
||||
) {
|
||||
setRESTSaveContext(null)
|
||||
}
|
||||
|
||||
return {
|
||||
state: newState,
|
||||
}
|
||||
},
|
||||
|
||||
moveRequest(
|
||||
{ state }: GraphqlCollectionStoreType,
|
||||
{
|
||||
path,
|
||||
requestIndex,
|
||||
destinationPath,
|
||||
}: { path: string; requestIndex: number; destinationPath: string }
|
||||
) {
|
||||
const newState = state
|
||||
|
||||
const indexPaths = path.split("/").map((x) => parseInt(x))
|
||||
|
||||
const targetLocation = navigateToFolderWithIndexPath(newState, indexPaths)
|
||||
|
||||
if (targetLocation === null) {
|
||||
console.log(
|
||||
`Could not resolve source path '${path}'. Skipping moveRequest dispatch.`
|
||||
)
|
||||
return {}
|
||||
}
|
||||
|
||||
const req = targetLocation.requests[requestIndex]
|
||||
|
||||
const destIndexPaths = destinationPath.split("/").map((x) => parseInt(x))
|
||||
|
||||
const destLocation = navigateToFolderWithIndexPath(newState, destIndexPaths)
|
||||
|
||||
if (destLocation === null) {
|
||||
console.log(
|
||||
`Could not resolve destination path '${destinationPath}'. Skipping moveRequest dispatch.`
|
||||
)
|
||||
return {}
|
||||
}
|
||||
|
||||
destLocation.requests.push(req)
|
||||
targetLocation.requests.splice(requestIndex, 1)
|
||||
|
||||
return {
|
||||
state: newState,
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const restCollectionStore = new DispatchingStore(
|
||||
defaultRESTCollectionState,
|
||||
restCollectionDispatchers
|
||||
)
|
||||
|
||||
export const graphqlCollectionStore = new DispatchingStore(
|
||||
defaultGraphqlCollectionState,
|
||||
gqlCollectionDispatchers
|
||||
)
|
||||
|
||||
export function setRESTCollections(entries: HoppCollection<HoppRESTRequest>[]) {
|
||||
restCollectionStore.dispatch({
|
||||
dispatcher: "setCollections",
|
||||
payload: {
|
||||
entries,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const restCollections$ = restCollectionStore.subject$.pipe(
|
||||
pluck("state")
|
||||
)
|
||||
|
||||
export const graphqlCollections$ = graphqlCollectionStore.subject$.pipe(
|
||||
pluck("state")
|
||||
)
|
||||
|
||||
export function appendRESTCollections(
|
||||
entries: HoppCollection<HoppRESTRequest>[]
|
||||
) {
|
||||
restCollectionStore.dispatch({
|
||||
dispatcher: "appendCollections",
|
||||
payload: {
|
||||
entries,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function addRESTCollection(collection: HoppCollection<HoppRESTRequest>) {
|
||||
restCollectionStore.dispatch({
|
||||
dispatcher: "addCollection",
|
||||
payload: {
|
||||
collection,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function removeRESTCollection(collectionIndex: number) {
|
||||
restCollectionStore.dispatch({
|
||||
dispatcher: "removeCollection",
|
||||
payload: {
|
||||
collectionIndex,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function getRESTCollection(collectionIndex: number) {
|
||||
return restCollectionStore.value.state[collectionIndex]
|
||||
}
|
||||
|
||||
export function editRESTCollection(
|
||||
collectionIndex: number,
|
||||
collection: HoppCollection<HoppRESTRequest>
|
||||
) {
|
||||
restCollectionStore.dispatch({
|
||||
dispatcher: "editCollection",
|
||||
payload: {
|
||||
collectionIndex,
|
||||
collection,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function addRESTFolder(name: string, path: string) {
|
||||
restCollectionStore.dispatch({
|
||||
dispatcher: "addFolder",
|
||||
payload: {
|
||||
name,
|
||||
path,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function editRESTFolder(
|
||||
path: string,
|
||||
folder: HoppCollection<HoppRESTRequest>
|
||||
) {
|
||||
restCollectionStore.dispatch({
|
||||
dispatcher: "editFolder",
|
||||
payload: {
|
||||
path,
|
||||
folder,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function removeRESTFolder(path: string) {
|
||||
restCollectionStore.dispatch({
|
||||
dispatcher: "removeFolder",
|
||||
payload: {
|
||||
path,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function editRESTRequest(
|
||||
path: string,
|
||||
requestIndex: number,
|
||||
requestNew: HoppRESTRequest
|
||||
) {
|
||||
const indexPaths = path.split("/").map((x) => parseInt(x))
|
||||
if (
|
||||
!navigateToFolderWithIndexPath(restCollectionStore.value.state, indexPaths)
|
||||
)
|
||||
throw new Error("Path not found")
|
||||
|
||||
restCollectionStore.dispatch({
|
||||
dispatcher: "editRequest",
|
||||
payload: {
|
||||
path,
|
||||
requestIndex,
|
||||
requestNew,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function saveRESTRequestAs(path: string, request: HoppRESTRequest) {
|
||||
// For calculating the insertion request index
|
||||
const targetLocation = navigateToFolderWithIndexPath(
|
||||
restCollectionStore.value.state,
|
||||
path.split("/").map((x) => parseInt(x))
|
||||
)
|
||||
|
||||
const insertionIndex = targetLocation!.requests.length
|
||||
|
||||
restCollectionStore.dispatch({
|
||||
dispatcher: "saveRequestAs",
|
||||
payload: {
|
||||
path,
|
||||
request,
|
||||
},
|
||||
})
|
||||
|
||||
return insertionIndex
|
||||
}
|
||||
|
||||
export function removeRESTRequest(path: string, requestIndex: number) {
|
||||
restCollectionStore.dispatch({
|
||||
dispatcher: "removeRequest",
|
||||
payload: {
|
||||
path,
|
||||
requestIndex,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function moveRESTRequest(
|
||||
path: string,
|
||||
requestIndex: number,
|
||||
destinationPath: string
|
||||
) {
|
||||
restCollectionStore.dispatch({
|
||||
dispatcher: "moveRequest",
|
||||
payload: {
|
||||
path,
|
||||
requestIndex,
|
||||
destinationPath,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setGraphqlCollections(
|
||||
entries: HoppCollection<HoppGQLRequest>[]
|
||||
) {
|
||||
graphqlCollectionStore.dispatch({
|
||||
dispatcher: "setCollections",
|
||||
payload: {
|
||||
entries,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function appendGraphqlCollections(
|
||||
entries: HoppCollection<HoppGQLRequest>[]
|
||||
) {
|
||||
graphqlCollectionStore.dispatch({
|
||||
dispatcher: "appendCollections",
|
||||
payload: {
|
||||
entries,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function addGraphqlCollection(
|
||||
collection: HoppCollection<HoppGQLRequest>
|
||||
) {
|
||||
graphqlCollectionStore.dispatch({
|
||||
dispatcher: "addCollection",
|
||||
payload: {
|
||||
collection,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function removeGraphqlCollection(collectionIndex: number) {
|
||||
graphqlCollectionStore.dispatch({
|
||||
dispatcher: "removeCollection",
|
||||
payload: {
|
||||
collectionIndex,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function editGraphqlCollection(
|
||||
collectionIndex: number,
|
||||
collection: HoppCollection<HoppGQLRequest>
|
||||
) {
|
||||
graphqlCollectionStore.dispatch({
|
||||
dispatcher: "editCollection",
|
||||
payload: {
|
||||
collectionIndex,
|
||||
collection,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function addGraphqlFolder(name: string, path: string) {
|
||||
graphqlCollectionStore.dispatch({
|
||||
dispatcher: "addFolder",
|
||||
payload: {
|
||||
name,
|
||||
path,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function editGraphqlFolder(
|
||||
path: string,
|
||||
folder: HoppCollection<HoppGQLRequest>
|
||||
) {
|
||||
graphqlCollectionStore.dispatch({
|
||||
dispatcher: "editFolder",
|
||||
payload: {
|
||||
path,
|
||||
folder,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function removeGraphqlFolder(path: string) {
|
||||
graphqlCollectionStore.dispatch({
|
||||
dispatcher: "removeFolder",
|
||||
payload: {
|
||||
path,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function editGraphqlRequest(
|
||||
path: string,
|
||||
requestIndex: number,
|
||||
requestNew: HoppGQLRequest
|
||||
) {
|
||||
graphqlCollectionStore.dispatch({
|
||||
dispatcher: "editRequest",
|
||||
payload: {
|
||||
path,
|
||||
requestIndex,
|
||||
requestNew,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function saveGraphqlRequestAs(path: string, request: HoppGQLRequest) {
|
||||
graphqlCollectionStore.dispatch({
|
||||
dispatcher: "saveRequestAs",
|
||||
payload: {
|
||||
path,
|
||||
request,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function removeGraphqlRequest(path: string, requestIndex: number) {
|
||||
graphqlCollectionStore.dispatch({
|
||||
dispatcher: "removeRequest",
|
||||
payload: {
|
||||
path,
|
||||
requestIndex,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function moveGraphqlRequest(
|
||||
path: string,
|
||||
requestIndex: number,
|
||||
destinationPath: string
|
||||
) {
|
||||
graphqlCollectionStore.dispatch({
|
||||
dispatcher: "moveRequest",
|
||||
payload: {
|
||||
path,
|
||||
requestIndex,
|
||||
destinationPath,
|
||||
},
|
||||
})
|
||||
}
|
||||
593
packages/hoppscotch-common/src/newstore/environments.ts
Normal file
593
packages/hoppscotch-common/src/newstore/environments.ts
Normal file
@@ -0,0 +1,593 @@
|
||||
import { Environment } from "@hoppscotch/data"
|
||||
import { cloneDeep, isEqual } from "lodash-es"
|
||||
import { combineLatest, Observable } from "rxjs"
|
||||
import { distinctUntilChanged, map, pluck } from "rxjs/operators"
|
||||
import DispatchingStore, {
|
||||
defineDispatchers,
|
||||
} from "~/newstore/DispatchingStore"
|
||||
|
||||
type SelectedEnvironmentIndex =
|
||||
| { type: "NO_ENV_SELECTED" }
|
||||
| { type: "MY_ENV"; index: number }
|
||||
| {
|
||||
type: "TEAM_ENV"
|
||||
teamID: string
|
||||
teamEnvID: string
|
||||
environment: Environment
|
||||
}
|
||||
|
||||
const defaultEnvironmentsState = {
|
||||
environments: [
|
||||
{
|
||||
name: "My Environment Variables",
|
||||
variables: [],
|
||||
},
|
||||
] as Environment[],
|
||||
|
||||
globals: [] as Environment["variables"],
|
||||
|
||||
selectedEnvironmentIndex: {
|
||||
type: "NO_ENV_SELECTED",
|
||||
} as SelectedEnvironmentIndex,
|
||||
}
|
||||
|
||||
type EnvironmentStore = typeof defaultEnvironmentsState
|
||||
|
||||
const dispatchers = defineDispatchers({
|
||||
setSelectedEnvironmentIndex(
|
||||
_: EnvironmentStore,
|
||||
{
|
||||
selectedEnvironmentIndex,
|
||||
}: { selectedEnvironmentIndex: SelectedEnvironmentIndex }
|
||||
) {
|
||||
return {
|
||||
selectedEnvironmentIndex,
|
||||
}
|
||||
},
|
||||
appendEnvironments(
|
||||
{ environments }: EnvironmentStore,
|
||||
{ envs }: { envs: Environment[] }
|
||||
) {
|
||||
return {
|
||||
environments: [...environments, ...envs],
|
||||
}
|
||||
},
|
||||
replaceEnvironments(
|
||||
_: EnvironmentStore,
|
||||
{ environments }: { environments: Environment[] }
|
||||
) {
|
||||
return {
|
||||
environments,
|
||||
}
|
||||
},
|
||||
createEnvironment(
|
||||
{ environments }: EnvironmentStore,
|
||||
{ name }: { name: string }
|
||||
) {
|
||||
return {
|
||||
environments: [
|
||||
...environments,
|
||||
{
|
||||
name,
|
||||
variables: [],
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
duplicateEnvironment(
|
||||
{ environments }: EnvironmentStore,
|
||||
{ envIndex }: { envIndex: number }
|
||||
) {
|
||||
const newEnvironment = environments.find((_, index) => index === envIndex)
|
||||
if (!newEnvironment) {
|
||||
return {
|
||||
environments,
|
||||
}
|
||||
}
|
||||
return {
|
||||
environments: [
|
||||
...environments,
|
||||
{
|
||||
...cloneDeep(newEnvironment),
|
||||
name: `${newEnvironment.name} - Duplicate`,
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
deleteEnvironment(
|
||||
{
|
||||
environments,
|
||||
// currentEnvironmentIndex,
|
||||
selectedEnvironmentIndex,
|
||||
}: EnvironmentStore,
|
||||
{ envIndex }: { envIndex: number }
|
||||
) {
|
||||
let newCurrEnvIndex = selectedEnvironmentIndex
|
||||
|
||||
// Scenario 1: Currently Selected Env is removed -> Set currently selected to none
|
||||
if (
|
||||
selectedEnvironmentIndex.type === "MY_ENV" &&
|
||||
envIndex === selectedEnvironmentIndex.index
|
||||
)
|
||||
newCurrEnvIndex = { type: "NO_ENV_SELECTED" }
|
||||
|
||||
// Scenario 2: Currently Selected Env Index > Deletion Index -> Current Selection Index Shifts One Position to the left -> Correct Env Index by moving back 1 index
|
||||
if (
|
||||
selectedEnvironmentIndex.type === "MY_ENV" &&
|
||||
envIndex < selectedEnvironmentIndex.index
|
||||
)
|
||||
newCurrEnvIndex = {
|
||||
type: "MY_ENV",
|
||||
index: selectedEnvironmentIndex.index - 1,
|
||||
}
|
||||
|
||||
// Scenario 3: Currently Selected Env Index < Deletion Index -> No change happens at selection position -> Noop
|
||||
return {
|
||||
environments: environments.filter((_, index) => index !== envIndex),
|
||||
selectedEnvironmentIndex: newCurrEnvIndex,
|
||||
}
|
||||
},
|
||||
renameEnvironment(
|
||||
{ environments }: EnvironmentStore,
|
||||
{ envIndex, newName }: { envIndex: number; newName: string }
|
||||
) {
|
||||
return {
|
||||
environments: environments.map((env, index) =>
|
||||
index === envIndex
|
||||
? {
|
||||
...env,
|
||||
name: newName,
|
||||
}
|
||||
: env
|
||||
),
|
||||
}
|
||||
},
|
||||
updateEnvironment(
|
||||
{ environments }: EnvironmentStore,
|
||||
{ envIndex, updatedEnv }: { envIndex: number; updatedEnv: Environment }
|
||||
) {
|
||||
return {
|
||||
environments: environments.map((env, index) =>
|
||||
index === envIndex ? updatedEnv : env
|
||||
),
|
||||
}
|
||||
},
|
||||
addEnvironmentVariable(
|
||||
{ environments }: EnvironmentStore,
|
||||
{ envIndex, key, value }: { envIndex: number; key: string; value: string }
|
||||
) {
|
||||
return {
|
||||
environments: environments.map((env, index) =>
|
||||
index === envIndex
|
||||
? {
|
||||
...env,
|
||||
variables: [...env.variables, { key, value }],
|
||||
}
|
||||
: env
|
||||
),
|
||||
}
|
||||
},
|
||||
removeEnvironmentVariable(
|
||||
{ environments }: EnvironmentStore,
|
||||
{ envIndex, variableIndex }: { envIndex: number; variableIndex: number }
|
||||
) {
|
||||
return {
|
||||
environments: environments.map((env, index) =>
|
||||
index === envIndex
|
||||
? {
|
||||
...env,
|
||||
variables: env.variables.filter(
|
||||
(_, vIndex) => vIndex !== variableIndex
|
||||
),
|
||||
}
|
||||
: env
|
||||
),
|
||||
}
|
||||
},
|
||||
setEnvironmentVariables(
|
||||
{ environments }: EnvironmentStore,
|
||||
{
|
||||
envIndex,
|
||||
vars,
|
||||
}: { envIndex: number; vars: { key: string; value: string }[] }
|
||||
) {
|
||||
return {
|
||||
environments: environments.map((env, index) =>
|
||||
index === envIndex
|
||||
? {
|
||||
...env,
|
||||
variables: vars,
|
||||
}
|
||||
: env
|
||||
),
|
||||
}
|
||||
},
|
||||
updateEnvironmentVariable(
|
||||
{ environments }: EnvironmentStore,
|
||||
{
|
||||
envIndex,
|
||||
variableIndex,
|
||||
updatedKey,
|
||||
updatedValue,
|
||||
}: {
|
||||
envIndex: number
|
||||
variableIndex: number
|
||||
updatedKey: string
|
||||
updatedValue: string
|
||||
}
|
||||
) {
|
||||
return {
|
||||
environments: environments.map((env, index) =>
|
||||
index === envIndex
|
||||
? {
|
||||
...env,
|
||||
variables: env.variables.map((v, vIndex) =>
|
||||
vIndex === variableIndex
|
||||
? { key: updatedKey, value: updatedValue }
|
||||
: v
|
||||
),
|
||||
}
|
||||
: env
|
||||
),
|
||||
}
|
||||
},
|
||||
setGlobalVariables(_, { entries }: { entries: Environment["variables"] }) {
|
||||
return {
|
||||
globals: entries,
|
||||
}
|
||||
},
|
||||
clearGlobalVariables() {
|
||||
return {
|
||||
globals: [],
|
||||
}
|
||||
},
|
||||
addGlobalVariable(
|
||||
{ globals },
|
||||
{ entry }: { entry: Environment["variables"][number] }
|
||||
) {
|
||||
return {
|
||||
globals: [...globals, entry],
|
||||
}
|
||||
},
|
||||
removeGlobalVariable({ globals }, { envIndex }: { envIndex: number }) {
|
||||
return {
|
||||
globals: globals.filter((_, i) => i !== envIndex),
|
||||
}
|
||||
},
|
||||
updateGlobalVariable(
|
||||
{ globals },
|
||||
{
|
||||
envIndex,
|
||||
updatedEntry,
|
||||
}: { envIndex: number; updatedEntry: Environment["variables"][number] }
|
||||
) {
|
||||
return {
|
||||
globals: globals.map((x, i) => (i !== envIndex ? x : updatedEntry)),
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const environmentsStore = new DispatchingStore(
|
||||
defaultEnvironmentsState,
|
||||
dispatchers
|
||||
)
|
||||
|
||||
export const environments$ = environmentsStore.subject$.pipe(
|
||||
pluck("environments"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const globalEnv$ = environmentsStore.subject$.pipe(
|
||||
pluck("globals"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const selectedEnvironmentIndex$ = environmentsStore.subject$.pipe(
|
||||
pluck("selectedEnvironmentIndex"),
|
||||
distinctUntilChanged()
|
||||
)
|
||||
|
||||
export const currentEnvironment$ = environmentsStore.subject$.pipe(
|
||||
map(({ environments, selectedEnvironmentIndex }) => {
|
||||
if (selectedEnvironmentIndex.type === "NO_ENV_SELECTED") {
|
||||
const env: Environment = {
|
||||
name: "No environment",
|
||||
variables: [],
|
||||
}
|
||||
return env
|
||||
} else if (selectedEnvironmentIndex.type === "MY_ENV") {
|
||||
return environments[selectedEnvironmentIndex.index]
|
||||
} else {
|
||||
return selectedEnvironmentIndex.environment
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
export type AggregateEnvironment = {
|
||||
key: string
|
||||
value: string
|
||||
sourceEnv: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream returning all the environment variables accessible in
|
||||
* the current state (Global + The Selected Environment).
|
||||
* NOTE: The source environment attribute will be "Global" for Global Env as source.
|
||||
*/
|
||||
export const aggregateEnvs$: Observable<AggregateEnvironment[]> = combineLatest(
|
||||
[currentEnvironment$, globalEnv$]
|
||||
).pipe(
|
||||
map(([selectedEnv, globalVars]) => {
|
||||
const results: AggregateEnvironment[] = []
|
||||
|
||||
selectedEnv.variables.forEach(({ key, value }) =>
|
||||
results.push({ key, value, sourceEnv: selectedEnv.name })
|
||||
)
|
||||
globalVars.forEach(({ key, value }) =>
|
||||
results.push({ key, value, sourceEnv: "Global" })
|
||||
)
|
||||
|
||||
return results
|
||||
}),
|
||||
distinctUntilChanged(isEqual)
|
||||
)
|
||||
|
||||
export function getAggregateEnvs() {
|
||||
const currentEnv = getCurrentEnvironment()
|
||||
|
||||
return [
|
||||
...currentEnv.variables.map(
|
||||
(x) =>
|
||||
<AggregateEnvironment>{
|
||||
key: x.key,
|
||||
value: x.value,
|
||||
sourceEnv: currentEnv.name,
|
||||
}
|
||||
),
|
||||
...getGlobalVariables().map(
|
||||
(x) =>
|
||||
<AggregateEnvironment>{
|
||||
key: x.key,
|
||||
value: x.value,
|
||||
sourceEnv: "Global",
|
||||
}
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
export function getCurrentEnvironment(): Environment {
|
||||
if (
|
||||
environmentsStore.value.selectedEnvironmentIndex.type === "NO_ENV_SELECTED"
|
||||
) {
|
||||
return {
|
||||
name: "No environment",
|
||||
variables: [],
|
||||
}
|
||||
} else if (
|
||||
environmentsStore.value.selectedEnvironmentIndex.type === "MY_ENV"
|
||||
) {
|
||||
return environmentsStore.value.environments[
|
||||
environmentsStore.value.selectedEnvironmentIndex.index
|
||||
]
|
||||
} else {
|
||||
return environmentsStore.value.selectedEnvironmentIndex.environment
|
||||
}
|
||||
}
|
||||
|
||||
export function getSelectedEnvironmentType() {
|
||||
return environmentsStore.value.selectedEnvironmentIndex.type
|
||||
}
|
||||
|
||||
export function setSelectedEnvironmentIndex(
|
||||
selectedEnvironmentIndex: SelectedEnvironmentIndex
|
||||
) {
|
||||
environmentsStore.dispatch({
|
||||
dispatcher: "setSelectedEnvironmentIndex",
|
||||
payload: {
|
||||
selectedEnvironmentIndex,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function getLegacyGlobalEnvironment(): Environment | null {
|
||||
const envs = environmentsStore.value.environments
|
||||
|
||||
const el = envs.find(
|
||||
(env) => env.name === "globals" || env.name === "Globals"
|
||||
)
|
||||
|
||||
return el ?? null
|
||||
}
|
||||
|
||||
export function getGlobalVariables(): Environment["variables"] {
|
||||
return environmentsStore.value.globals
|
||||
}
|
||||
|
||||
export function addGlobalEnvVariable(entry: Environment["variables"][number]) {
|
||||
environmentsStore.dispatch({
|
||||
dispatcher: "addGlobalVariable",
|
||||
payload: {
|
||||
entry,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setGlobalEnvVariables(entries: Environment["variables"]) {
|
||||
environmentsStore.dispatch({
|
||||
dispatcher: "setGlobalVariables",
|
||||
payload: {
|
||||
entries,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function clearGlobalEnvVariables() {
|
||||
environmentsStore.dispatch({
|
||||
dispatcher: "clearGlobalVariables",
|
||||
payload: {},
|
||||
})
|
||||
}
|
||||
|
||||
export function removeGlobalEnvVariable(envIndex: number) {
|
||||
environmentsStore.dispatch({
|
||||
dispatcher: "removeGlobalVariable",
|
||||
payload: {
|
||||
envIndex,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function updateGlobalEnvVariable(
|
||||
envIndex: number,
|
||||
updatedEntry: Environment["variables"][number]
|
||||
) {
|
||||
environmentsStore.dispatch({
|
||||
dispatcher: "updateGlobalVariable",
|
||||
payload: {
|
||||
envIndex,
|
||||
updatedEntry,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function replaceEnvironments(newEnvironments: any[]) {
|
||||
environmentsStore.dispatch({
|
||||
dispatcher: "replaceEnvironments",
|
||||
payload: {
|
||||
environments: newEnvironments,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function appendEnvironments(envs: Environment[]) {
|
||||
environmentsStore.dispatch({
|
||||
dispatcher: "appendEnvironments",
|
||||
payload: {
|
||||
envs,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function createEnvironment(envName: string) {
|
||||
environmentsStore.dispatch({
|
||||
dispatcher: "createEnvironment",
|
||||
payload: {
|
||||
name: envName,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function duplicateEnvironment(envIndex: number) {
|
||||
environmentsStore.dispatch({
|
||||
dispatcher: "duplicateEnvironment",
|
||||
payload: {
|
||||
envIndex,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteEnvironment(envIndex: number) {
|
||||
environmentsStore.dispatch({
|
||||
dispatcher: "deleteEnvironment",
|
||||
payload: {
|
||||
envIndex,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function renameEnvironment(envIndex: number, newName: string) {
|
||||
environmentsStore.dispatch({
|
||||
dispatcher: "renameEnvironment",
|
||||
payload: {
|
||||
envIndex,
|
||||
newName,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function updateEnvironment(envIndex: number, updatedEnv: Environment) {
|
||||
environmentsStore.dispatch({
|
||||
dispatcher: "updateEnvironment",
|
||||
payload: {
|
||||
envIndex,
|
||||
updatedEnv,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function setEnvironmentVariables(
|
||||
envIndex: number,
|
||||
vars: { key: string; value: string }[]
|
||||
) {
|
||||
environmentsStore.dispatch({
|
||||
dispatcher: "setEnvironmentVariables",
|
||||
payload: {
|
||||
envIndex,
|
||||
vars,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function addEnvironmentVariable(
|
||||
envIndex: number,
|
||||
{ key, value }: { key: string; value: string }
|
||||
) {
|
||||
environmentsStore.dispatch({
|
||||
dispatcher: "addEnvironmentVariable",
|
||||
payload: {
|
||||
envIndex,
|
||||
key,
|
||||
value,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function removeEnvironmentVariable(
|
||||
envIndex: number,
|
||||
variableIndex: number
|
||||
) {
|
||||
environmentsStore.dispatch({
|
||||
dispatcher: "removeEnvironmentVariable",
|
||||
payload: {
|
||||
envIndex,
|
||||
variableIndex,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function updateEnvironmentVariable(
|
||||
envIndex: number,
|
||||
variableIndex: number,
|
||||
{ key, value }: { key: string; value: string }
|
||||
) {
|
||||
environmentsStore.dispatch({
|
||||
dispatcher: "updateEnvironmentVariable",
|
||||
payload: {
|
||||
envIndex,
|
||||
variableIndex,
|
||||
updatedKey: key,
|
||||
updatedValue: value,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
type SelectedEnv =
|
||||
| { type: "NO_ENV_SELECTED" }
|
||||
| { type: "MY_ENV"; index: number }
|
||||
| { type: "TEAM_ENV" }
|
||||
|
||||
export function getEnvironment(selectedEnv: SelectedEnv) {
|
||||
if (selectedEnv.type === "MY_ENV") {
|
||||
return environmentsStore.value.environments[selectedEnv.index]
|
||||
} else if (
|
||||
selectedEnv.type === "TEAM_ENV" &&
|
||||
environmentsStore.value.selectedEnvironmentIndex.type === "TEAM_ENV"
|
||||
) {
|
||||
return environmentsStore.value.selectedEnvironmentIndex.environment
|
||||
} else {
|
||||
return {
|
||||
name: "N0_ENV",
|
||||
variables: [],
|
||||
}
|
||||
}
|
||||
}
|
||||
331
packages/hoppscotch-common/src/newstore/history.ts
Normal file
331
packages/hoppscotch-common/src/newstore/history.ts
Normal file
@@ -0,0 +1,331 @@
|
||||
import { isEqual } from "lodash-es"
|
||||
import { pluck } from "rxjs/operators"
|
||||
import {
|
||||
HoppRESTRequest,
|
||||
translateToNewRequest,
|
||||
HoppGQLRequest,
|
||||
translateToGQLRequest,
|
||||
GQL_REQ_SCHEMA_VERSION,
|
||||
} from "@hoppscotch/data"
|
||||
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
|
||||
import { completedRESTResponse$ } from "./RESTSession"
|
||||
|
||||
export type RESTHistoryEntry = {
|
||||
v: number
|
||||
|
||||
request: HoppRESTRequest
|
||||
|
||||
responseMeta: {
|
||||
duration: number | null
|
||||
statusCode: number | null
|
||||
}
|
||||
|
||||
star: boolean
|
||||
|
||||
id?: string // For when Firebase Firestore is set
|
||||
|
||||
updatedOn?: Date
|
||||
}
|
||||
|
||||
export type GQLHistoryEntry = {
|
||||
v: number
|
||||
request: HoppGQLRequest
|
||||
|
||||
response: string
|
||||
|
||||
star: boolean
|
||||
|
||||
id?: string // For when Firestore ID is set
|
||||
|
||||
updatedOn?: Date
|
||||
}
|
||||
|
||||
export function makeRESTHistoryEntry(
|
||||
x: Omit<RESTHistoryEntry, "v">
|
||||
): RESTHistoryEntry {
|
||||
return {
|
||||
v: 1,
|
||||
...x,
|
||||
}
|
||||
}
|
||||
|
||||
export function makeGQLHistoryEntry(
|
||||
x: Omit<GQLHistoryEntry, "v">
|
||||
): GQLHistoryEntry {
|
||||
return {
|
||||
v: 1,
|
||||
...x,
|
||||
updatedOn: new Date(),
|
||||
}
|
||||
}
|
||||
|
||||
export function translateToNewRESTHistory(x: any): RESTHistoryEntry {
|
||||
if (x.v === 1) return x
|
||||
|
||||
// Legacy
|
||||
const request = translateToNewRequest(x)
|
||||
const star = x.star ?? false
|
||||
const duration = x.duration ?? null
|
||||
const statusCode = x.status ?? null
|
||||
const updatedOn = x.updatedOn ?? null
|
||||
const obj: RESTHistoryEntry = makeRESTHistoryEntry({
|
||||
request,
|
||||
star,
|
||||
responseMeta: {
|
||||
duration,
|
||||
statusCode,
|
||||
},
|
||||
updatedOn,
|
||||
})
|
||||
|
||||
if (x.id) obj.id = x.id
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
export function translateToNewGQLHistory(x: any): GQLHistoryEntry {
|
||||
if (x.v === 1 && x.request.v === GQL_REQ_SCHEMA_VERSION) return x
|
||||
|
||||
// Legacy
|
||||
const request = x.request
|
||||
? translateToGQLRequest(x.request)
|
||||
: translateToGQLRequest(x)
|
||||
const star = x.star ?? false
|
||||
const response = x.response ?? ""
|
||||
const updatedOn = x.updatedOn ?? ""
|
||||
|
||||
const obj: GQLHistoryEntry = makeGQLHistoryEntry({
|
||||
request,
|
||||
star,
|
||||
response,
|
||||
updatedOn,
|
||||
})
|
||||
|
||||
if (x.id) obj.id = x.id
|
||||
|
||||
return obj
|
||||
}
|
||||
|
||||
export const defaultRESTHistoryState = {
|
||||
state: [] as RESTHistoryEntry[],
|
||||
}
|
||||
|
||||
export const defaultGraphqlHistoryState = {
|
||||
state: [] as GQLHistoryEntry[],
|
||||
}
|
||||
|
||||
export const HISTORY_LIMIT = 50
|
||||
|
||||
type RESTHistoryType = typeof defaultRESTHistoryState
|
||||
type GraphqlHistoryType = typeof defaultGraphqlHistoryState
|
||||
|
||||
const RESTHistoryDispatchers = defineDispatchers({
|
||||
setEntries(_: RESTHistoryType, { entries }: { entries: RESTHistoryEntry[] }) {
|
||||
return {
|
||||
state: entries,
|
||||
}
|
||||
},
|
||||
addEntry(
|
||||
currentVal: RESTHistoryType,
|
||||
{ entry }: { entry: RESTHistoryEntry }
|
||||
) {
|
||||
return {
|
||||
state: [entry, ...currentVal.state].slice(0, HISTORY_LIMIT),
|
||||
}
|
||||
},
|
||||
deleteEntry(
|
||||
currentVal: RESTHistoryType,
|
||||
{ entry }: { entry: RESTHistoryEntry }
|
||||
) {
|
||||
return {
|
||||
state: currentVal.state.filter((e) => !isEqual(e, entry)),
|
||||
}
|
||||
},
|
||||
clearHistory() {
|
||||
return {
|
||||
state: [],
|
||||
}
|
||||
},
|
||||
toggleStar(
|
||||
currentVal: RESTHistoryType,
|
||||
{ entry }: { entry: RESTHistoryEntry }
|
||||
) {
|
||||
return {
|
||||
state: currentVal.state.map((e) => {
|
||||
if (isEqual(e, entry) && e.star !== undefined) {
|
||||
return {
|
||||
...e,
|
||||
star: !e.star,
|
||||
}
|
||||
}
|
||||
return e
|
||||
}),
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const GQLHistoryDispatchers = defineDispatchers({
|
||||
setEntries(
|
||||
_: GraphqlHistoryType,
|
||||
{ entries }: { entries: GQLHistoryEntry[] }
|
||||
) {
|
||||
return {
|
||||
state: entries,
|
||||
}
|
||||
},
|
||||
addEntry(
|
||||
currentVal: GraphqlHistoryType,
|
||||
{ entry }: { entry: GQLHistoryEntry }
|
||||
) {
|
||||
return {
|
||||
state: [entry, ...currentVal.state].slice(0, HISTORY_LIMIT),
|
||||
}
|
||||
},
|
||||
deleteEntry(
|
||||
currentVal: GraphqlHistoryType,
|
||||
{ entry }: { entry: GQLHistoryEntry }
|
||||
) {
|
||||
return {
|
||||
state: currentVal.state.filter((e) => !isEqual(e, entry)),
|
||||
}
|
||||
},
|
||||
clearHistory() {
|
||||
return {
|
||||
state: [],
|
||||
}
|
||||
},
|
||||
toggleStar(
|
||||
currentVal: GraphqlHistoryType,
|
||||
{ entry }: { entry: GQLHistoryEntry }
|
||||
) {
|
||||
return {
|
||||
state: currentVal.state.map((e) => {
|
||||
if (isEqual(e, entry) && e.star !== undefined) {
|
||||
return {
|
||||
...e,
|
||||
star: !e.star,
|
||||
}
|
||||
}
|
||||
return e
|
||||
}),
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
export const restHistoryStore = new DispatchingStore(
|
||||
defaultRESTHistoryState,
|
||||
RESTHistoryDispatchers
|
||||
)
|
||||
|
||||
export const graphqlHistoryStore = new DispatchingStore(
|
||||
defaultGraphqlHistoryState,
|
||||
GQLHistoryDispatchers
|
||||
)
|
||||
|
||||
export const restHistory$ = restHistoryStore.subject$.pipe(pluck("state"))
|
||||
export const graphqlHistory$ = graphqlHistoryStore.subject$.pipe(pluck("state"))
|
||||
|
||||
export function setRESTHistoryEntries(entries: RESTHistoryEntry[]) {
|
||||
restHistoryStore.dispatch({
|
||||
dispatcher: "setEntries",
|
||||
payload: { entries },
|
||||
})
|
||||
}
|
||||
|
||||
export function addRESTHistoryEntry(entry: RESTHistoryEntry) {
|
||||
restHistoryStore.dispatch({
|
||||
dispatcher: "addEntry",
|
||||
payload: { entry },
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteRESTHistoryEntry(entry: RESTHistoryEntry) {
|
||||
restHistoryStore.dispatch({
|
||||
dispatcher: "deleteEntry",
|
||||
payload: { entry },
|
||||
})
|
||||
}
|
||||
|
||||
export function clearRESTHistory() {
|
||||
restHistoryStore.dispatch({
|
||||
dispatcher: "clearHistory",
|
||||
payload: {},
|
||||
})
|
||||
}
|
||||
|
||||
export function toggleRESTHistoryEntryStar(entry: RESTHistoryEntry) {
|
||||
restHistoryStore.dispatch({
|
||||
dispatcher: "toggleStar",
|
||||
payload: { entry },
|
||||
})
|
||||
}
|
||||
|
||||
export function setGraphqlHistoryEntries(entries: GQLHistoryEntry[]) {
|
||||
graphqlHistoryStore.dispatch({
|
||||
dispatcher: "setEntries",
|
||||
payload: { entries },
|
||||
})
|
||||
}
|
||||
|
||||
export function addGraphqlHistoryEntry(entry: GQLHistoryEntry) {
|
||||
graphqlHistoryStore.dispatch({
|
||||
dispatcher: "addEntry",
|
||||
payload: { entry },
|
||||
})
|
||||
}
|
||||
|
||||
export function deleteGraphqlHistoryEntry(entry: GQLHistoryEntry) {
|
||||
graphqlHistoryStore.dispatch({
|
||||
dispatcher: "deleteEntry",
|
||||
payload: { entry },
|
||||
})
|
||||
}
|
||||
|
||||
export function clearGraphqlHistory() {
|
||||
graphqlHistoryStore.dispatch({
|
||||
dispatcher: "clearHistory",
|
||||
payload: {},
|
||||
})
|
||||
}
|
||||
|
||||
export function toggleGraphqlHistoryEntryStar(entry: GQLHistoryEntry) {
|
||||
graphqlHistoryStore.dispatch({
|
||||
dispatcher: "toggleStar",
|
||||
payload: { entry },
|
||||
})
|
||||
}
|
||||
|
||||
// Listen to completed responses to add to history
|
||||
completedRESTResponse$.subscribe((res) => {
|
||||
if (res !== null) {
|
||||
if (
|
||||
res.type === "loading" ||
|
||||
res.type === "network_fail" ||
|
||||
res.type === "script_fail"
|
||||
)
|
||||
return
|
||||
|
||||
addRESTHistoryEntry(
|
||||
makeRESTHistoryEntry({
|
||||
request: {
|
||||
auth: res.req.auth,
|
||||
body: res.req.body,
|
||||
endpoint: res.req.endpoint,
|
||||
headers: res.req.headers,
|
||||
method: res.req.method,
|
||||
name: res.req.name,
|
||||
params: res.req.params,
|
||||
preRequestScript: res.req.preRequestScript,
|
||||
testScript: res.req.testScript,
|
||||
v: res.req.v,
|
||||
},
|
||||
responseMeta: {
|
||||
duration: res.meta.responseDuration,
|
||||
statusCode: res.statusCode,
|
||||
},
|
||||
star: false,
|
||||
updatedOn: new Date(),
|
||||
})
|
||||
)
|
||||
}
|
||||
})
|
||||
390
packages/hoppscotch-common/src/newstore/localpersistence.ts
Normal file
390
packages/hoppscotch-common/src/newstore/localpersistence.ts
Normal file
@@ -0,0 +1,390 @@
|
||||
/* eslint-disable no-restricted-globals, no-restricted-syntax */
|
||||
|
||||
import { clone, cloneDeep, assign, isEmpty } from "lodash-es"
|
||||
import * as O from "fp-ts/Option"
|
||||
import { pipe } from "fp-ts/function"
|
||||
import {
|
||||
safelyExtractRESTRequest,
|
||||
translateToNewRequest,
|
||||
translateToNewRESTCollection,
|
||||
translateToNewGQLCollection,
|
||||
Environment,
|
||||
} from "@hoppscotch/data"
|
||||
import {
|
||||
settingsStore,
|
||||
bulkApplySettings,
|
||||
defaultSettings,
|
||||
applySetting,
|
||||
HoppAccentColor,
|
||||
HoppBgColor,
|
||||
} from "./settings"
|
||||
import {
|
||||
restHistoryStore,
|
||||
graphqlHistoryStore,
|
||||
setRESTHistoryEntries,
|
||||
setGraphqlHistoryEntries,
|
||||
translateToNewRESTHistory,
|
||||
translateToNewGQLHistory,
|
||||
} from "./history"
|
||||
import {
|
||||
restCollectionStore,
|
||||
graphqlCollectionStore,
|
||||
setGraphqlCollections,
|
||||
setRESTCollections,
|
||||
} from "./collections"
|
||||
import {
|
||||
replaceEnvironments,
|
||||
environments$,
|
||||
addGlobalEnvVariable,
|
||||
setGlobalEnvVariables,
|
||||
globalEnv$,
|
||||
setSelectedEnvironmentIndex,
|
||||
selectedEnvironmentIndex$,
|
||||
} from "./environments"
|
||||
import {
|
||||
getDefaultRESTRequest,
|
||||
restRequest$,
|
||||
setRESTRequest,
|
||||
} from "./RESTSession"
|
||||
import { WSRequest$, setWSRequest } from "./WebSocketSession"
|
||||
import { SIORequest$, setSIORequest } from "./SocketIOSession"
|
||||
import { SSERequest$, setSSERequest } from "./SSESession"
|
||||
import { MQTTRequest$, setMQTTRequest } from "./MQTTSession"
|
||||
import { bulkApplyLocalState, localStateStore } from "./localstate"
|
||||
import { StorageLike } from "@vueuse/core"
|
||||
|
||||
function checkAndMigrateOldSettings() {
|
||||
const vuexData = JSON.parse(window.localStorage.getItem("vuex") || "{}")
|
||||
if (isEmpty(vuexData)) return
|
||||
|
||||
const { postwoman } = vuexData
|
||||
|
||||
if (!isEmpty(postwoman?.settings)) {
|
||||
const settingsData = assign(clone(defaultSettings), postwoman.settings)
|
||||
|
||||
window.localStorage.setItem("settings", JSON.stringify(settingsData))
|
||||
|
||||
delete postwoman.settings
|
||||
window.localStorage.setItem("vuex", JSON.stringify(vuexData))
|
||||
}
|
||||
|
||||
if (postwoman?.collections) {
|
||||
window.localStorage.setItem(
|
||||
"collections",
|
||||
JSON.stringify(postwoman.collections)
|
||||
)
|
||||
|
||||
delete postwoman.collections
|
||||
window.localStorage.setItem("vuex", JSON.stringify(vuexData))
|
||||
}
|
||||
|
||||
if (postwoman?.collectionsGraphql) {
|
||||
window.localStorage.setItem(
|
||||
"collectionsGraphql",
|
||||
JSON.stringify(postwoman.collectionsGraphql)
|
||||
)
|
||||
|
||||
delete postwoman.collectionsGraphql
|
||||
window.localStorage.setItem("vuex", JSON.stringify(vuexData))
|
||||
}
|
||||
|
||||
if (postwoman?.environments) {
|
||||
window.localStorage.setItem(
|
||||
"environments",
|
||||
JSON.stringify(postwoman.environments)
|
||||
)
|
||||
|
||||
delete postwoman.environments
|
||||
window.localStorage.setItem("vuex", JSON.stringify(vuexData))
|
||||
}
|
||||
|
||||
if (window.localStorage.getItem("THEME_COLOR")) {
|
||||
const themeColor = window.localStorage.getItem("THEME_COLOR")
|
||||
applySetting("THEME_COLOR", themeColor as HoppAccentColor)
|
||||
|
||||
window.localStorage.removeItem("THEME_COLOR")
|
||||
}
|
||||
|
||||
if (window.localStorage.getItem("nuxt-color-mode")) {
|
||||
const color = window.localStorage.getItem("nuxt-color-mode") as HoppBgColor
|
||||
applySetting("BG_COLOR", color)
|
||||
|
||||
window.localStorage.removeItem("nuxt-color-mode")
|
||||
}
|
||||
}
|
||||
|
||||
function setupLocalStatePersistence() {
|
||||
const localStateData = JSON.parse(
|
||||
window.localStorage.getItem("localState") ?? "{}"
|
||||
)
|
||||
|
||||
if (localStateData) bulkApplyLocalState(localStateData)
|
||||
|
||||
localStateStore.subject$.subscribe((state) => {
|
||||
window.localStorage.setItem("localState", JSON.stringify(state))
|
||||
})
|
||||
}
|
||||
|
||||
function setupSettingsPersistence() {
|
||||
const settingsData = JSON.parse(
|
||||
window.localStorage.getItem("settings") || "{}"
|
||||
)
|
||||
|
||||
if (settingsData) {
|
||||
bulkApplySettings(settingsData)
|
||||
}
|
||||
|
||||
settingsStore.subject$.subscribe((settings) => {
|
||||
window.localStorage.setItem("settings", JSON.stringify(settings))
|
||||
})
|
||||
}
|
||||
|
||||
function setupHistoryPersistence() {
|
||||
const restHistoryData = JSON.parse(
|
||||
window.localStorage.getItem("history") || "[]"
|
||||
).map(translateToNewRESTHistory)
|
||||
|
||||
const graphqlHistoryData = JSON.parse(
|
||||
window.localStorage.getItem("graphqlHistory") || "[]"
|
||||
).map(translateToNewGQLHistory)
|
||||
|
||||
setRESTHistoryEntries(restHistoryData)
|
||||
setGraphqlHistoryEntries(graphqlHistoryData)
|
||||
|
||||
restHistoryStore.subject$.subscribe(({ state }) => {
|
||||
window.localStorage.setItem("history", JSON.stringify(state))
|
||||
})
|
||||
|
||||
graphqlHistoryStore.subject$.subscribe(({ state }) => {
|
||||
window.localStorage.setItem("graphqlHistory", JSON.stringify(state))
|
||||
})
|
||||
}
|
||||
|
||||
function setupCollectionsPersistence() {
|
||||
const restCollectionData = JSON.parse(
|
||||
window.localStorage.getItem("collections") || "[]"
|
||||
).map(translateToNewRESTCollection)
|
||||
|
||||
const graphqlCollectionData = JSON.parse(
|
||||
window.localStorage.getItem("collectionsGraphql") || "[]"
|
||||
).map(translateToNewGQLCollection)
|
||||
|
||||
setRESTCollections(restCollectionData)
|
||||
setGraphqlCollections(graphqlCollectionData)
|
||||
|
||||
restCollectionStore.subject$.subscribe(({ state }) => {
|
||||
window.localStorage.setItem("collections", JSON.stringify(state))
|
||||
})
|
||||
|
||||
graphqlCollectionStore.subject$.subscribe(({ state }) => {
|
||||
window.localStorage.setItem("collectionsGraphql", JSON.stringify(state))
|
||||
})
|
||||
}
|
||||
|
||||
function setupEnvironmentsPersistence() {
|
||||
const environmentsData: Environment[] = JSON.parse(
|
||||
window.localStorage.getItem("environments") || "[]"
|
||||
)
|
||||
|
||||
// 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(
|
||||
"environments",
|
||||
JSON.stringify(environmentsData)
|
||||
)
|
||||
}
|
||||
|
||||
replaceEnvironments(environmentsData)
|
||||
|
||||
environments$.subscribe((envs) => {
|
||||
window.localStorage.setItem("environments", JSON.stringify(envs))
|
||||
})
|
||||
}
|
||||
|
||||
function setupSelectedEnvPersistence() {
|
||||
const selectedEnvIndex = pipe(
|
||||
// Value from local storage can be nullable
|
||||
O.fromNullable(window.localStorage.getItem("selectedEnvIndex")),
|
||||
O.map(parseInt), // If not null, parse to integer
|
||||
O.chain(
|
||||
O.fromPredicate(
|
||||
Number.isInteger // Check if the number is proper int (not NaN)
|
||||
)
|
||||
),
|
||||
O.getOrElse(() => -1) // If all the above conditions pass, we are good, else set default value (-1)
|
||||
)
|
||||
// Check if current environment index is -1 ie. no environment is selected
|
||||
if (selectedEnvIndex === -1) {
|
||||
setSelectedEnvironmentIndex({
|
||||
type: "NO_ENV_SELECTED",
|
||||
})
|
||||
} else {
|
||||
setSelectedEnvironmentIndex({
|
||||
type: "MY_ENV",
|
||||
index: selectedEnvIndex,
|
||||
})
|
||||
}
|
||||
|
||||
selectedEnvironmentIndex$.subscribe((envIndex) => {
|
||||
if (envIndex.type === "MY_ENV") {
|
||||
window.localStorage.setItem("selectedEnvIndex", envIndex.index.toString())
|
||||
} else {
|
||||
window.localStorage.setItem("selectedEnvIndex", "-1")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function setupWebsocketPersistence() {
|
||||
const request = JSON.parse(
|
||||
window.localStorage.getItem("WebsocketRequest") || "null"
|
||||
)
|
||||
|
||||
setWSRequest(request)
|
||||
|
||||
WSRequest$.subscribe((req) => {
|
||||
window.localStorage.setItem("WebsocketRequest", JSON.stringify(req))
|
||||
})
|
||||
}
|
||||
|
||||
function setupSocketIOPersistence() {
|
||||
const request = JSON.parse(
|
||||
window.localStorage.getItem("SocketIORequest") || "null"
|
||||
)
|
||||
|
||||
setSIORequest(request)
|
||||
|
||||
SIORequest$.subscribe((req) => {
|
||||
window.localStorage.setItem("SocketIORequest", JSON.stringify(req))
|
||||
})
|
||||
}
|
||||
|
||||
function setupSSEPersistence() {
|
||||
const request = JSON.parse(
|
||||
window.localStorage.getItem("SSERequest") || "null"
|
||||
)
|
||||
|
||||
setSSERequest(request)
|
||||
|
||||
SSERequest$.subscribe((req) => {
|
||||
window.localStorage.setItem("SSERequest", JSON.stringify(req))
|
||||
})
|
||||
}
|
||||
|
||||
function setupMQTTPersistence() {
|
||||
const request = JSON.parse(
|
||||
window.localStorage.getItem("MQTTRequest") || "null"
|
||||
)
|
||||
|
||||
setMQTTRequest(request)
|
||||
|
||||
MQTTRequest$.subscribe((req) => {
|
||||
window.localStorage.setItem("MQTTRequest", JSON.stringify(req))
|
||||
})
|
||||
}
|
||||
|
||||
function setupGlobalEnvsPersistence() {
|
||||
const globals: Environment["variables"] = JSON.parse(
|
||||
window.localStorage.getItem("globalEnv") || "[]"
|
||||
)
|
||||
|
||||
setGlobalEnvVariables(globals)
|
||||
|
||||
globalEnv$.subscribe((vars) => {
|
||||
window.localStorage.setItem("globalEnv", JSON.stringify(vars))
|
||||
})
|
||||
}
|
||||
|
||||
function setupRequestPersistence() {
|
||||
const localRequest = JSON.parse(
|
||||
window.localStorage.getItem("restRequest") || "null"
|
||||
)
|
||||
|
||||
if (localRequest) {
|
||||
const parsedLocal = translateToNewRequest(localRequest)
|
||||
setRESTRequest(
|
||||
safelyExtractRESTRequest(parsedLocal, getDefaultRESTRequest())
|
||||
)
|
||||
}
|
||||
|
||||
restRequest$.subscribe((req) => {
|
||||
const reqClone = cloneDeep(req)
|
||||
if (reqClone.body.contentType === "multipart/form-data") {
|
||||
reqClone.body.body = reqClone.body.body.map((x) => {
|
||||
if (x.isFile)
|
||||
return {
|
||||
...x,
|
||||
isFile: false,
|
||||
value: "",
|
||||
}
|
||||
else return x
|
||||
})
|
||||
}
|
||||
window.localStorage.setItem("restRequest", JSON.stringify(reqClone))
|
||||
})
|
||||
}
|
||||
|
||||
export function setupLocalPersistence() {
|
||||
checkAndMigrateOldSettings()
|
||||
|
||||
setupLocalStatePersistence()
|
||||
setupSettingsPersistence()
|
||||
setupRequestPersistence()
|
||||
setupHistoryPersistence()
|
||||
setupCollectionsPersistence()
|
||||
setupGlobalEnvsPersistence()
|
||||
setupEnvironmentsPersistence()
|
||||
setupSelectedEnvPersistence()
|
||||
setupWebsocketPersistence()
|
||||
setupSocketIOPersistence()
|
||||
setupSSEPersistence()
|
||||
setupMQTTPersistence()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a value in LocalStorage.
|
||||
*
|
||||
* NOTE: Use LocalStorage to only store non-reactive simple data
|
||||
* For more complex data, use stores and connect it to localpersistence
|
||||
*/
|
||||
export function 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 localpersistence
|
||||
*/
|
||||
export function setLocalConfig(key: string, value: string) {
|
||||
window.localStorage.setItem(key, value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear config value in LocalStorage.
|
||||
* @param key Key to be cleared
|
||||
*/
|
||||
export function removeLocalConfig(key: string) {
|
||||
window.localStorage.removeItem(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* The storage system we are using in the application.
|
||||
* NOTE: This is a placeholder for being used in app.
|
||||
* This entire redirection of localStorage is to allow for
|
||||
* not refactoring the entire app code when we refactor when
|
||||
* we are building the native (which may lack localStorage,
|
||||
* or use a custom system)
|
||||
*/
|
||||
export const hoppLocalConfigStorage: StorageLike = localStorage
|
||||
67
packages/hoppscotch-common/src/newstore/localstate.ts
Normal file
67
packages/hoppscotch-common/src/newstore/localstate.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { Ref } from "vue"
|
||||
import { distinctUntilChanged, pluck } from "rxjs"
|
||||
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
|
||||
import { useStream } from "@composables/stream"
|
||||
|
||||
type LocalState = {
|
||||
REMEMBERED_TEAM_ID: string | undefined
|
||||
}
|
||||
|
||||
const defaultLocalState: LocalState = {
|
||||
REMEMBERED_TEAM_ID: undefined,
|
||||
}
|
||||
|
||||
const dispatchers = defineDispatchers({
|
||||
bulkApplyState(_currentState: LocalState, payload: Partial<LocalState>) {
|
||||
return payload
|
||||
},
|
||||
applyState<K extends keyof LocalState>(
|
||||
_currentState: LocalState,
|
||||
{ key, value }: { key: K; value: LocalState[K] }
|
||||
) {
|
||||
const result: Partial<LocalState> = {
|
||||
[key]: value,
|
||||
}
|
||||
|
||||
return result
|
||||
},
|
||||
})
|
||||
|
||||
export const localStateStore = new DispatchingStore(
|
||||
defaultLocalState,
|
||||
dispatchers
|
||||
)
|
||||
|
||||
export const localState$ = localStateStore.subject$.asObservable()
|
||||
|
||||
export function bulkApplyLocalState(obj: Partial<LocalState>) {
|
||||
localStateStore.dispatch({
|
||||
dispatcher: "bulkApplyState",
|
||||
payload: obj,
|
||||
})
|
||||
}
|
||||
|
||||
export function applyLocalState<K extends keyof LocalState>(
|
||||
key: K,
|
||||
value: LocalState[K]
|
||||
) {
|
||||
localStateStore.dispatch({
|
||||
dispatcher: "applyState",
|
||||
payload: { key, value },
|
||||
})
|
||||
}
|
||||
|
||||
export function useLocalState<K extends keyof LocalState>(
|
||||
key: K
|
||||
): Ref<LocalState[K]> {
|
||||
return useStream(
|
||||
localStateStore.subject$.pipe(pluck(key), distinctUntilChanged()),
|
||||
localStateStore.value[key],
|
||||
(value: LocalState[K]) => {
|
||||
localStateStore.dispatch({
|
||||
dispatcher: "applyState",
|
||||
payload: { key, value },
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
165
packages/hoppscotch-common/src/newstore/settings.ts
Normal file
165
packages/hoppscotch-common/src/newstore/settings.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { pluck, distinctUntilChanged } from "rxjs/operators"
|
||||
import { has } from "lodash-es"
|
||||
import { Observable } from "rxjs"
|
||||
|
||||
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
|
||||
import type { KeysMatching } from "~/types/ts-utils"
|
||||
|
||||
export const HoppBgColors = ["system", "light", "dark", "black"] as const
|
||||
|
||||
export type HoppBgColor = typeof HoppBgColors[number]
|
||||
|
||||
export const HoppAccentColors = [
|
||||
"green",
|
||||
"teal",
|
||||
"blue",
|
||||
"indigo",
|
||||
"purple",
|
||||
"yellow",
|
||||
"orange",
|
||||
"red",
|
||||
"pink",
|
||||
] as const
|
||||
|
||||
export type HoppAccentColor = typeof HoppAccentColors[number]
|
||||
|
||||
export const HoppFontSizes = ["small", "medium", "large"] as const
|
||||
|
||||
export type HoppFontSize = typeof HoppFontSizes[number]
|
||||
|
||||
export type SettingsType = {
|
||||
syncCollections: boolean
|
||||
syncHistory: boolean
|
||||
syncEnvironments: boolean
|
||||
|
||||
PROXY_ENABLED: boolean
|
||||
PROXY_URL: string
|
||||
EXTENSIONS_ENABLED: boolean
|
||||
URL_EXCLUDES: {
|
||||
auth: boolean
|
||||
httpUser: boolean
|
||||
httpPassword: boolean
|
||||
bearerToken: boolean
|
||||
oauth2Token: boolean
|
||||
}
|
||||
THEME_COLOR: HoppAccentColor
|
||||
BG_COLOR: HoppBgColor
|
||||
TELEMETRY_ENABLED: boolean
|
||||
EXPAND_NAVIGATION: boolean
|
||||
SIDEBAR: boolean
|
||||
SIDEBAR_ON_LEFT: boolean
|
||||
ZEN_MODE: boolean
|
||||
FONT_SIZE: HoppFontSize
|
||||
COLUMN_LAYOUT: boolean
|
||||
}
|
||||
|
||||
export const defaultSettings: SettingsType = {
|
||||
syncCollections: true,
|
||||
syncHistory: true,
|
||||
syncEnvironments: true,
|
||||
|
||||
PROXY_ENABLED: false,
|
||||
PROXY_URL: "https://proxy.hoppscotch.io/",
|
||||
EXTENSIONS_ENABLED: false,
|
||||
URL_EXCLUDES: {
|
||||
auth: true,
|
||||
httpUser: true,
|
||||
httpPassword: true,
|
||||
bearerToken: true,
|
||||
oauth2Token: true,
|
||||
},
|
||||
THEME_COLOR: "indigo",
|
||||
BG_COLOR: "system",
|
||||
TELEMETRY_ENABLED: true,
|
||||
EXPAND_NAVIGATION: true,
|
||||
SIDEBAR: true,
|
||||
SIDEBAR_ON_LEFT: true,
|
||||
ZEN_MODE: false,
|
||||
FONT_SIZE: "small",
|
||||
COLUMN_LAYOUT: true,
|
||||
}
|
||||
|
||||
const validKeys = Object.keys(defaultSettings)
|
||||
|
||||
const dispatchers = defineDispatchers({
|
||||
bulkApplySettings(
|
||||
_currentState: SettingsType,
|
||||
payload: Partial<SettingsType>
|
||||
) {
|
||||
return payload
|
||||
},
|
||||
toggleSetting(
|
||||
currentState: SettingsType,
|
||||
{ settingKey }: { settingKey: KeysMatching<SettingsType, boolean> }
|
||||
) {
|
||||
if (!has(currentState, settingKey)) {
|
||||
// console.log(
|
||||
// `Toggling of a non-existent setting key '${settingKey}' ignored`
|
||||
// )
|
||||
return {}
|
||||
}
|
||||
|
||||
const result: Partial<SettingsType> = {}
|
||||
result[settingKey] = !currentState[settingKey]
|
||||
|
||||
return result
|
||||
},
|
||||
applySetting<K extends keyof SettingsType>(
|
||||
_currentState: SettingsType,
|
||||
{ settingKey, value }: { settingKey: K; value: SettingsType[K] }
|
||||
) {
|
||||
if (!validKeys.includes(settingKey)) {
|
||||
// console.log(
|
||||
// `Ignoring non-existent setting key '${settingKey}' assignment`
|
||||
// )
|
||||
return {}
|
||||
}
|
||||
|
||||
const result: Partial<SettingsType> = {}
|
||||
result[settingKey] = value
|
||||
|
||||
return result
|
||||
},
|
||||
})
|
||||
|
||||
export const settingsStore = new DispatchingStore(defaultSettings, dispatchers)
|
||||
|
||||
/**
|
||||
* An observable value to make avail all the state information at once
|
||||
*/
|
||||
export const settings$ = settingsStore.subject$.asObservable()
|
||||
|
||||
export function getSettingSubject<K extends keyof SettingsType>(
|
||||
settingKey: K
|
||||
): Observable<SettingsType[K]> {
|
||||
return settingsStore.subject$.pipe(pluck(settingKey), distinctUntilChanged())
|
||||
}
|
||||
|
||||
export function bulkApplySettings(settingsObj: Partial<SettingsType>) {
|
||||
settingsStore.dispatch({
|
||||
dispatcher: "bulkApplySettings",
|
||||
payload: settingsObj,
|
||||
})
|
||||
}
|
||||
|
||||
export function toggleSetting(settingKey: KeysMatching<SettingsType, boolean>) {
|
||||
settingsStore.dispatch({
|
||||
dispatcher: "toggleSetting",
|
||||
payload: {
|
||||
settingKey,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export function applySetting<K extends keyof SettingsType>(
|
||||
settingKey: K,
|
||||
value: SettingsType[K]
|
||||
) {
|
||||
settingsStore.dispatch({
|
||||
dispatcher: "applySetting",
|
||||
payload: {
|
||||
settingKey,
|
||||
value,
|
||||
},
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user