feat: initial desktop app commit

This commit is contained in:
Andrew Bastin
2023-10-11 12:04:43 +05:30
parent e2b15cedd4
commit 4587cee189
119 changed files with 18023 additions and 94 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,5 @@
mutation ClearGlobalEnvironments($id: ID!) {
clearGlobalEnvironments(id: $id) {
id
}
}

View File

@@ -0,0 +1,11 @@
mutation CreateGQLChildUserCollection(
$title: String!
$parentUserCollectionID: ID!
) {
createGQLChildUserCollection(
title: $title
parentUserCollectionID: $parentUserCollectionID
) {
id
}
}

View File

@@ -0,0 +1,5 @@
mutation CreateGQLRootUserCollection($title: String!) {
createGQLRootUserCollection(title: $title) {
id
}
}

View File

@@ -0,0 +1,13 @@
mutation CreateGQLUserRequest(
$title: String!
$request: String!
$collectionID: ID!
) {
createGQLUserRequest(
title: $title
request: $request
collectionID: $collectionID
) {
id
}
}

View File

@@ -0,0 +1,11 @@
mutation CreateRESTChildUserCollection(
$title: String!
$parentUserCollectionID: ID!
) {
createRESTChildUserCollection(
title: $title
parentUserCollectionID: $parentUserCollectionID
) {
id
}
}

View File

@@ -0,0 +1,5 @@
mutation CreateRESTRootUserCollection($title: String!) {
createRESTRootUserCollection(title: $title) {
id
}
}

View File

@@ -0,0 +1,13 @@
mutation CreateRESTUserRequest(
$collectionID: ID!
$title: String!
$request: String!
) {
createRESTUserRequest(
collectionID: $collectionID
title: $title
request: $request
) {
id
}
}

View File

@@ -0,0 +1,9 @@
mutation CreateUserEnvironment($name: String!, $variables: String!) {
createUserEnvironment(name: $name, variables: $variables) {
id
userUid
name
variables
isGlobal
}
}

View File

@@ -0,0 +1,5 @@
mutation CreateUserGlobalEnvironment($variables: String!) {
createUserGlobalEnvironment(variables: $variables) {
id
}
}

View File

@@ -0,0 +1,13 @@
mutation CreateUserHistory(
$reqData: String!
$resMetadata: String!
$reqType: ReqType!
) {
createUserHistory(
reqData: $reqData
resMetadata: $resMetadata
reqType: $reqType
) {
id
}
}

View File

@@ -0,0 +1,5 @@
mutation CreateUserSettings($properties: String!) {
createUserSettings(properties: $properties) {
id
}
}

View File

@@ -0,0 +1,6 @@
mutation DeleteAllUserHistory($reqType: ReqType!) {
deleteAllUserHistory(reqType: $reqType) {
count
reqType
}
}

View File

@@ -0,0 +1,3 @@
mutation DeleteUserCollection($userCollectionID: ID!) {
deleteUserCollection(userCollectionID: $userCollectionID)
}

View File

@@ -0,0 +1,3 @@
mutation DeleteUserEnvironment($id: ID!) {
deleteUserEnvironment(id: $id)
}

View File

@@ -0,0 +1,3 @@
mutation DeleteUserRequest($requestID: ID!) {
deleteUserRequest(id: $requestID)
}

View File

@@ -0,0 +1,8 @@
mutation MoveUserCollection($destCollectionID: ID, $userCollectionID: ID!) {
moveUserCollection(
destCollectionID: $destCollectionID
userCollectionID: $userCollectionID
) {
id
}
}

View File

@@ -0,0 +1,15 @@
mutation MoveUserRequest(
$sourceCollectionID: ID!
$requestID: ID!
$destinationCollectionID: ID!
$nextRequestID: ID
) {
moveUserRequest(
sourceCollectionID: $sourceCollectionID
requestID: $requestID
destinationCollectionID: $destinationCollectionID
nextRequestID: $nextRequestID
) {
id
}
}

View File

@@ -0,0 +1,5 @@
mutation RemoveRequestFromHistory($id: ID!) {
removeRequestFromHistory(id: $id) {
id
}
}

View File

@@ -0,0 +1,8 @@
mutation RenameUserCollection($userCollectionID: ID!, $newTitle: String!) {
renameUserCollection(
userCollectionID: $userCollectionID
newTitle: $newTitle
) {
id
}
}

View File

@@ -0,0 +1,5 @@
mutation ToggleHistoryStarStatus($id: ID!) {
toggleHistoryStarStatus(id: $id) {
id
}
}

View File

@@ -0,0 +1,5 @@
mutation UpdateGQLUserRequest($id: ID!, $request: String!, $title: String) {
updateGQLUserRequest(id: $id, request: $request, title: $title) {
id
}
}

View File

@@ -0,0 +1,7 @@
mutation UpdateRESTUserRequest($id: ID!, $title: String!, $request: String!) {
updateRESTUserRequest(id: $id, title: $title, request: $request) {
id
collectionID
request
}
}

View File

@@ -0,0 +1,6 @@
mutation UpdateUserCollectionOrder($collectionID: ID!, $nextCollectionID: ID) {
updateUserCollectionOrder(
collectionID: $collectionID
nextCollectionID: $nextCollectionID
)
}

View File

@@ -0,0 +1,9 @@
mutation UpdateUserEnvironment($id: ID!, $name: String!, $variables: String!) {
updateUserEnvironment(id: $id, name: $name, variables: $variables) {
id
userUid
name
variables
isGlobal
}
}

View File

@@ -0,0 +1,11 @@
mutation UpdateUserSession(
$currentSession: String!
$sessionType: SessionType!
) {
updateUserSessions(
currentSession: $currentSession
sessionType: $sessionType
) {
currentRESTSession
}
}

View File

@@ -0,0 +1,5 @@
mutation UpdateUserSettings($properties: String!) {
updateUserSettings(properties: $properties) {
id
}
}

View File

@@ -0,0 +1,9 @@
mutation CreateUserEnvironment($name: String!, $variables: String!) {
createUserEnvironment(name: $name, variables: $variables) {
id
userUid
name
variables
isGlobal
}
}

View File

@@ -0,0 +1,12 @@
query ExportUserCollectionsToJSON(
$collectionID: ID
$collectionType: ReqType!
) {
exportUserCollectionsToJSON(
collectionID: $collectionID
collectionType: $collectionType
) {
collectionType
exportedCollection
}
}

View File

@@ -0,0 +1,5 @@
query GetCurrentRESTSession {
me {
currentRESTSession
}
}

View File

@@ -0,0 +1,11 @@
query GetGlobalEnvironments {
me {
globalEnvironments {
id
isGlobal
name
userUid
variables
}
}
}

View File

@@ -0,0 +1,23 @@
query GetRESTUserHistory {
me {
RESTHistory {
id
userUid
reqType
request
responseMetadata
isStarred
executedOn
}
GQLHistory {
id
userUid
reqType
request
responseMetadata
isStarred
executedOn
}
}
}

View File

@@ -0,0 +1,13 @@
query GetGQLRootUserCollections {
# the frontend doesnt paginate right now, so giving take a big enough value to get all collections at once
rootGQLUserCollections(take: 99999) {
id
title
type
childrenGQL {
id
title
type
}
}
}

View File

@@ -0,0 +1,11 @@
query GetUserEnvironments {
me {
environments {
id
isGlobal
name
userUid
variables
}
}
}

View File

@@ -0,0 +1,13 @@
query GetUserRootCollections {
# the frontend doesnt paginate right now, so giving take a big enough value to get all collections at once
rootRESTUserCollections(take: 99999) {
id
title
type
childrenREST {
id
title
type
}
}
}

View File

@@ -0,0 +1,8 @@
query GetUserSettings {
me {
settings {
id
properties
}
}
}

View File

@@ -0,0 +1,10 @@
subscription UserCollectionCreated {
userCollectionCreated {
parent {
id
}
id
title
type
}
}

View File

@@ -0,0 +1,9 @@
subscription UserCollectionMoved {
userCollectionMoved {
id
parent {
id
}
type
}
}

View File

@@ -0,0 +1,17 @@
subscription UserCollectionOrderUpdated {
userCollectionOrderUpdated {
userCollection {
id
parent {
id
}
}
nextUserCollection {
id
parent {
id
}
}
}
}

View File

@@ -0,0 +1,6 @@
subscription UserCollectionRemoved {
userCollectionRemoved {
id
type
}
}

View File

@@ -0,0 +1,10 @@
subscription userCollectionUpdated {
userCollectionUpdated {
id
title
type
parent {
id
}
}
}

View File

@@ -0,0 +1,9 @@
subscription UserEnvironmentCreated {
userEnvironmentCreated {
id
isGlobal
name
userUid
variables
}
}

View File

@@ -0,0 +1,5 @@
subscription UserEnvironmentDeleted {
userEnvironmentDeleted {
id
}
}

View File

@@ -0,0 +1,9 @@
subscription UserEnvironmentUpdated {
userEnvironmentUpdated {
id
userUid
name
variables
isGlobal
}
}

View File

@@ -0,0 +1,10 @@
subscription UserHistoryCreated {
userHistoryCreated {
id
reqType
request
responseMetadata
isStarred
executedOn
}
}

View File

@@ -0,0 +1,6 @@
subscription userHistoryDeleted {
userHistoryDeleted {
id
reqType
}
}

View File

@@ -0,0 +1,6 @@
subscription UserHistoryDeletedMany {
userHistoryDeletedMany {
count
reqType
}
}

View File

@@ -0,0 +1,10 @@
subscription UserHistoryUpdated {
userHistoryUpdated {
id
reqType
request
responseMetadata
isStarred
executedOn
}
}

View File

@@ -0,0 +1,9 @@
subscription UserRequestCreated {
userRequestCreated {
id
collectionID
title
request
type
}
}

View File

@@ -0,0 +1,9 @@
subscription UserRequestDeleted {
userRequestDeleted {
id
collectionID
title
request
type
}
}

View File

@@ -0,0 +1,13 @@
subscription UserRequestMoved {
userRequestMoved {
request {
id
collectionID
type
}
nextRequest {
id
collectionID
}
}
}

View File

@@ -0,0 +1,9 @@
subscription UserRequestUpdated {
userRequestUpdated {
id
collectionID
title
request
type
}
}

View File

@@ -0,0 +1,6 @@
subscription UserSettingsUpdated {
userSettingsUpdated {
id
properties
}
}

View File

@@ -0,0 +1,102 @@
import { Observable } from "rxjs"
import DispatchingStore from "@hoppscotch/common/newstore/DispatchingStore"
export type DispatchersOf<T extends DispatchingStore<any, any>> =
T extends DispatchingStore<any, infer U>
? U extends Record<infer D, any>
? D
: never
: never
export type StoreSyncDefinitionOf<T extends DispatchingStore<any, any>> = {
[x in DispatchersOf<T>]?: T extends DispatchingStore<any, infer U>
? U extends Record<x, any>
? U[x] extends (x: any, y: infer Y) => any
? (payload: Y) => void
: never
: never
: never
}
let _isRunningDispatchWithoutSyncing = true
export function runDispatchWithOutSyncing(func: () => void) {
_isRunningDispatchWithoutSyncing = false
func()
_isRunningDispatchWithoutSyncing = true
}
export const getSyncInitFunction = <T extends DispatchingStore<any, any>>(
store: T,
storeSyncDefinition: StoreSyncDefinitionOf<T>,
shouldSyncValue: () => boolean,
shouldSyncObservable?: Observable<boolean>
) => {
let startSubscriptions: () => () => void | undefined
let stopSubscriptions: () => void | undefined
let oldSyncStatus = shouldSyncValue()
// Start and stop the subscriptions according to the sync settings from profile
shouldSyncObservable &&
shouldSyncObservable.subscribe((newSyncStatus) => {
if (oldSyncStatus === true && newSyncStatus === false) {
stopListeningToSubscriptions()
} else if (oldSyncStatus === false && newSyncStatus === true) {
startListeningToSubscriptions()
}
oldSyncStatus = newSyncStatus
})
function startStoreSync() {
store.dispatches$.subscribe((actionParams) => {
// typescript cannot understand that the dispatcher can be the index, so casting to any
if ((storeSyncDefinition as any)[actionParams.dispatcher]) {
const dispatcher = actionParams.dispatcher
const payload = actionParams.payload
const operationMapperFunction = (storeSyncDefinition as any)[dispatcher]
if (
operationMapperFunction &&
_isRunningDispatchWithoutSyncing &&
shouldSyncValue()
) {
operationMapperFunction(payload)
}
}
})
}
function setupSubscriptions(func: () => () => void) {
startSubscriptions = func
}
function startListeningToSubscriptions() {
if (!startSubscriptions) {
console.warn(
"We don't have a function to start subscriptions. Please use `setupSubscriptions` to setup the start function."
)
}
stopSubscriptions = startSubscriptions()
}
function stopListeningToSubscriptions() {
if (!stopSubscriptions) {
console.warn(
"We don't have a function to unsubscribe. make sure you return the unsubscribe function when using setupSubscriptions"
)
}
stopSubscriptions()
}
return {
startStoreSync,
setupSubscriptions,
startListeningToSubscriptions,
stopListeningToSubscriptions,
}
}

View File

@@ -0,0 +1,42 @@
export const createMapper = <
LocalIDType extends string | number,
BackendIDType extends string | number
>() => {
const backendIDByLocalIDMap = new Map<
LocalIDType,
BackendIDType | undefined
>()
const localIDByBackendIDMap = new Map<
BackendIDType,
LocalIDType | undefined
>()
return {
addEntry(localIdentifier: LocalIDType, backendIdentifier: BackendIDType) {
backendIDByLocalIDMap.set(localIdentifier, backendIdentifier)
localIDByBackendIDMap.set(backendIdentifier, localIdentifier)
},
getValue() {
return backendIDByLocalIDMap
},
getBackendIDByLocalID(localIdentifier: LocalIDType) {
return backendIDByLocalIDMap.get(localIdentifier)
},
getLocalIDByBackendID(backendId: BackendIDType) {
return localIDByBackendIDMap.get(backendId)
},
removeEntry(backendId?: BackendIDType, index?: LocalIDType) {
if (backendId) {
const index = localIDByBackendIDMap.get(backendId)
localIDByBackendIDMap.delete(backendId)
index && backendIDByLocalIDMap.delete(index)
} else if (index) {
const backendId = backendIDByLocalIDMap.get(index)
backendIDByLocalIDMap.delete(index)
backendId && localIDByBackendIDMap.delete(backendId)
}
},
}
}

View File

@@ -0,0 +1,114 @@
import { createHoppApp } from "@hoppscotch/common"
import { def as authDef } from "./platform/auth"
import { def as environmentsDef } from "./platform/environments/environments.platform"
import { def as collectionsDef } from "./platform/collections/collections.platform"
import { def as settingsDef } from "./platform/settings/settings.platform"
import { def as historyDef } from "./platform/history/history.platform"
import { def as tabStateDef } from "./platform/tabState/tabState.platform"
import { localclientInterceptor } from "./platform/std/interceptors/localclient"
import { browserInterceptor } from "@hoppscotch/common/platform/std/interceptors/browser"
import { proxyInterceptor } from "@hoppscotch/common/platform/std/interceptors/proxy"
import { ExtensionInspectorService } from "@hoppscotch/common/platform/std/inspections/extension.inspector"
import { ExtensionInterceptorService } from "@hoppscotch/common/platform/std/interceptors/extension"
import { nextTick, ref, watch } from "vue"
import { emit, listen } from "@tauri-apps/api/event"
import { type } from "@tauri-apps/api/os"
import { useSettingStatic } from "@hoppscotch/common/composables/settings"
import { appWindow } from "@tauri-apps/api/window"
import { stdFooterItems } from "@hoppscotch/common/platform/std/ui/footerItem"
import { stdSupportOptionItems } from "@hoppscotch/common/platform/std/ui/supportOptionsItem"
import { useMousePressed } from "@vueuse/core"
const headerPaddingLeft = ref("0px")
const headerPaddingTop = ref("0px")
createHoppApp("#app", {
ui: {
additionalFooterMenuItems: stdFooterItems,
additionalSupportOptionsMenuItems: stdSupportOptionItems,
appHeader: {
paddingLeft: headerPaddingLeft,
paddingTop: headerPaddingTop,
},
},
auth: authDef,
sync: {
environments: environmentsDef,
collections: collectionsDef,
settings: settingsDef,
history: historyDef,
tabState: tabStateDef,
},
interceptors: {
default: "localclient",
interceptors: [
{ type: "standalone", interceptor: localclientInterceptor },
{ type: "standalone", interceptor: browserInterceptor },
{ type: "standalone", interceptor: proxyInterceptor },
{ type: "service", service: ExtensionInterceptorService },
],
},
additionalInspectors: [
{ type: "service", service: ExtensionInspectorService },
],
platformFeatureFlags: {
exportAsGIST: false,
hasTelemetry: false,
},
})
watch(
useSettingStatic("BG_COLOR")[0],
async () => {
await nextTick()
await emit(
"hopp-bg-changed",
getComputedStyle(document.documentElement).getPropertyValue(
"--primary-color"
)
)
},
{ immediate: true }
)
;(async () => {
const platform = await type()
if (platform === "Darwin") {
listen("will-enter-fullscreen", () => {
headerPaddingTop.value = "0px"
headerPaddingLeft.value = "0px"
})
listen("will-exit-fullscreen", () => {
headerPaddingTop.value = "2px"
headerPaddingLeft.value = "70px"
})
headerPaddingTop.value = "2px"
headerPaddingLeft.value = "70px"
}
})()
const { pressed } = useMousePressed()
document.addEventListener("mousemove", (ev) => {
const { clientX, clientY } = ev
const el = document.querySelector("header")
if (!el) return
const { left, top, width, height } = el.getBoundingClientRect()
if (
clientX >= left &&
clientX <= left + width &&
clientY >= top &&
clientY <= top + height
) {
if (pressed.value) {
appWindow.startDragging()
}
}
})

View File

@@ -0,0 +1,373 @@
import axios from "axios"
import {
AuthEvent,
AuthPlatformDef,
HoppUser,
} from "@hoppscotch/common/platform/auth"
import { BehaviorSubject, Subject } from "rxjs"
import {
getLocalConfig,
removeLocalConfig,
setLocalConfig,
} from "@hoppscotch/common/newstore/localpersistence"
import { Ref, ref, watch } from "vue"
import { open } from '@tauri-apps/api/shell'
import { Body, getClient } from '@tauri-apps/api/http'
import { listen } from '@tauri-apps/api/event'
import { Store } from "tauri-plugin-store-api";
import { P } from "@tauri-apps/api/event-41a9edf5"
export const authEvents$ = new Subject<AuthEvent | { event: "token_refresh" }>()
const currentUser$ = new BehaviorSubject<HoppUser | null>(null)
export const probableUser$ = new BehaviorSubject<HoppUser | null>(null)
async function logout() {
let client = await getClient();
await client.get(`${import.meta.env.VITE_BACKEND_API_URL}/auth/logout`)
const store = new Store("/Users/vivek/.creds.dat")
await store.set("refresh_token", {})
await store.set("access_token", {})
await store.save()
}
async function signInUserWithGithubFB() {
await open(`${import.meta.env.VITE_BACKEND_API_URL}/auth/github?redirect_uri=desktop`);
}
async function signInUserWithGoogleFB() {
await open(`${import.meta.env.VITE_BACKEND_API_URL}/auth/google?redirect_uri=desktop`);
}
async function signInUserWithMicrosoftFB() {
await open(`${import.meta.env.VITE_BACKEND_API_URL}/auth/microsoft?redirect_uri=desktop`);
}
async function getInitialUserDetails() {
const store = new Store("/Users/vivek/.creds.dat");
try {
const accessToken = await store.get("access_token")
let client = await getClient()
let body = {
query: `query Me {
me {
uid
displayName
email
photoURL
isAdmin
createdOn
}
}`}
let res = await client.post(`${import.meta.env.VITE_BACKEND_GQL_URL}`,
Body.json(body), {
headers: {
"Cookie": `access_token=${accessToken.value}`,
}
}
)
return res.data
} catch (error) {
let res = {
error: "auth/cookies_not_found"
}
return res
}
}
const isGettingInitialUser: Ref<null | boolean> = ref(null)
function setUser(user: HoppUser | null) {
currentUser$.next(user)
probableUser$.next(user)
setLocalConfig("login_state", JSON.stringify(user))
}
async function setInitialUser() {
isGettingInitialUser.value = true
const res = await getInitialUserDetails()
const error = res.errors && res.errors[0]
// no cookies sent. so the user is not logged in
if (error && error.message === "auth/cookies_not_found") {
setUser(null)
isGettingInitialUser.value = false
return
}
if (error && error.message === "user/not_found") {
setUser(null)
isGettingInitialUser.value = false
return
}
// cookies sent, but it is expired, we need to refresh the token
if (error && error.message === "Unauthorized") {
const isRefreshSuccess = await refreshToken()
if (isRefreshSuccess) {
setInitialUser()
} else {
setUser(null)
isGettingInitialUser.value = false
}
return
}
// no errors, we have a valid user
if (res.data && res.data.me) {
const hoppBackendUser = res.data.me
const hoppUser: HoppUser = {
uid: hoppBackendUser.uid,
displayName: hoppBackendUser.displayName,
email: hoppBackendUser.email,
photoURL: hoppBackendUser.photoURL,
// all our signin methods currently guarantees the email is verified
emailVerified: true,
}
setUser(hoppUser)
isGettingInitialUser.value = false
authEvents$.next({
event: "login",
user: hoppUser,
})
return
}
}
async function refreshToken() {
const store = new Store("/Users/vivek/.creds.dat");
try {
const refreshToken = await store.get("refresh_token")
let client = await getClient()
let res = await client.get(`${import.meta.env.VITE_BACKEND_API_URL}/auth/refresh`, {
headers: { "Cookie": `refresh_token=${refreshToken.value}` }
})
setAuthCookies(res.rawHeaders)
const isSuccessful = res.status === 200
if (isSuccessful) {
authEvents$.next({
event: "token_refresh",
})
}
return isSuccessful
} catch (err) {
return false
}
}
async function sendMagicLink(email: string) {
const client = await getClient();
let url = `${import.meta.env.VITE_BACKEND_API_URL}/auth/signin?origin=desktop`;
const res = await client.post(url, Body.json({ email }));
if (res.data && res.data.deviceIdentifier) {
setLocalConfig("deviceIdentifier", res.data.deviceIdentifier)
} else {
throw new Error("test: does not get device identifier")
}
return res.data
}
async function setAuthCookies(rawHeaders: Array<String>) {
let cookies = rawHeaders['set-cookie'].join("|")
const accessTokenMatch = cookies.match(/access_token=([^;]+)/);
const refreshTokenMatch = cookies.match(/refresh_token=([^;]+)/);
const store = new Store("/Users/vivek/.creds.dat")
if (accessTokenMatch) {
const accessToken = accessTokenMatch[1];
await store.set("access_token", { value: accessToken })
}
if (refreshTokenMatch) {
const refreshToken = refreshTokenMatch[1];
await store.set("refresh_token", { value: refreshToken })
}
await store.save()
}
export const def: AuthPlatformDef = {
getCurrentUserStream: () => currentUser$,
getAuthEventsStream: () => authEvents$,
getProbableUserStream: () => probableUser$,
getCurrentUser: () => currentUser$.value,
getProbableUser: () => probableUser$.value,
getBackendHeaders() {
return {}
},
getGQLClientOptions() {
return {
fetchOptions: {
credentials: "include",
},
}
},
/**
* it is not possible for us to know if the current cookie is expired because we cannot access http-only cookies from js
* hence just returning if the currentUser$ has a value associated with it
*/
willBackendHaveAuthError() {
return !currentUser$.value
},
// eslint-disable-next-line @typescript-eslint/no-unused-vars
onBackendGQLClientShouldReconnect(func) {
authEvents$.subscribe((event) => {
if (
event.event == "login" ||
event.event == "logout" ||
event.event == "token_refresh"
) {
func()
}
})
},
/**
* we cannot access our auth cookies from javascript, so leaving this as null
*/
getDevOptsBackendIDToken() {
return null
},
async performAuthInit() {
const probableUser = JSON.parse(getLocalConfig("login_state") ?? "null")
probableUser$.next(probableUser)
await setInitialUser()
await listen('scheme-request-received', async (event: any) => {
let deep_link = event.payload as string;
const params = new URLSearchParams(deep_link.split('?')[1]);
const accessToken = params.get('access_token');
const refreshToken = params.get('refresh_token');
const token = params.get('token');
function isNotNullOrUndefined(x: any) {
return x !== null && x !== undefined;
}
if (isNotNullOrUndefined(accessToken) && isNotNullOrUndefined(refreshToken)) {
const store = new Store("/Users/vivek/.creds.dat")
await store.set("access_token", { value: accessToken });
await store.set("refresh_token", { value: refreshToken } );
await store.save()
window.location.href = "/"
return;
}
if (isNotNullOrUndefined(token)) {
setLocalConfig("verifyToken", token)
await this.signInWithEmailLink("", "")
await setInitialUser()
}
});
},
waitProbableLoginToConfirm() {
return new Promise<void>((resolve, reject) => {
if (this.getCurrentUser()) {
resolve()
}
if (!probableUser$.value) reject(new Error("no_probable_user"))
const unwatch = watch(isGettingInitialUser, (val) => {
if (val === true || val === false) {
resolve()
unwatch()
}
})
})
},
async signInWithEmail(email: string) {
await sendMagicLink(email)
},
async verifyEmailAddress() {
return
},
async signInUserWithGoogle() {
await signInUserWithGoogleFB()
},
async signInUserWithGithub() {
await signInUserWithGithubFB()
return undefined
},
async signInUserWithMicrosoft() {
await signInUserWithMicrosoftFB()
},
async signInWithEmailLink(_email, _url) {
const deviceIdentifier = getLocalConfig("deviceIdentifier")
if (!deviceIdentifier) {
throw new Error(
"Device Identifier not found, you can only signin from the browser you generated the magic link"
)
}
let verifyToken = getLocalConfig("verifyToken")
const client = await getClient();
let res = await client.post(`${import.meta.env.VITE_BACKEND_API_URL}/auth/verify`, Body.json({
token: verifyToken,
deviceIdentifier
}));
setAuthCookies(res.rawHeaders)
removeLocalConfig("deviceIdentifier")
removeLocalConfig("verifyToken")
window.location.href = "/"
},
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async setEmailAddress(_email: string) {
return
},
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async setDisplayName(name: string) {
return
},
async signOutUser() {
// if (!currentUser$.value) throw new Error("No user has logged in")
await logout()
probableUser$.next(null)
currentUser$.next(null)
removeLocalConfig("login_state")
authEvents$.next({
event: "logout",
})
},
}

View File

@@ -0,0 +1,305 @@
import {
runGQLQuery,
runGQLSubscription,
runMutation,
} from "@hoppscotch/common/helpers/backend/GQLClient"
import {
CreateRestRootUserCollectionDocument,
CreateRestRootUserCollectionMutation,
CreateRestRootUserCollectionMutationVariables,
CreateRestUserRequestMutation,
CreateRestUserRequestMutationVariables,
CreateRestUserRequestDocument,
CreateRestChildUserCollectionMutation,
CreateRestChildUserCollectionMutationVariables,
CreateRestChildUserCollectionDocument,
DeleteUserCollectionMutation,
DeleteUserCollectionMutationVariables,
DeleteUserCollectionDocument,
RenameUserCollectionMutation,
RenameUserCollectionMutationVariables,
RenameUserCollectionDocument,
MoveUserCollectionMutation,
MoveUserCollectionMutationVariables,
MoveUserCollectionDocument,
DeleteUserRequestMutation,
DeleteUserRequestMutationVariables,
DeleteUserRequestDocument,
MoveUserRequestDocument,
MoveUserRequestMutation,
MoveUserRequestMutationVariables,
UpdateUserCollectionOrderMutation,
UpdateUserCollectionOrderMutationVariables,
UpdateUserCollectionOrderDocument,
GetUserRootCollectionsQuery,
GetUserRootCollectionsQueryVariables,
GetUserRootCollectionsDocument,
UserCollectionCreatedDocument,
UserCollectionUpdatedDocument,
UserCollectionRemovedDocument,
UserCollectionMovedDocument,
UserCollectionOrderUpdatedDocument,
ExportUserCollectionsToJsonQuery,
ExportUserCollectionsToJsonQueryVariables,
ExportUserCollectionsToJsonDocument,
UserRequestCreatedDocument,
UserRequestUpdatedDocument,
UserRequestMovedDocument,
UserRequestDeletedDocument,
UpdateRestUserRequestMutation,
UpdateRestUserRequestMutationVariables,
UpdateRestUserRequestDocument,
CreateGqlRootUserCollectionMutation,
CreateGqlRootUserCollectionMutationVariables,
CreateGqlRootUserCollectionDocument,
CreateGqlUserRequestMutation,
CreateGqlUserRequestMutationVariables,
CreateGqlUserRequestDocument,
CreateGqlChildUserCollectionMutation,
CreateGqlChildUserCollectionMutationVariables,
CreateGqlChildUserCollectionDocument,
UpdateGqlUserRequestMutation,
UpdateGqlUserRequestMutationVariables,
UpdateGqlUserRequestDocument,
GetGqlRootUserCollectionsQuery,
GetGqlRootUserCollectionsQueryVariables,
GetGqlRootUserCollectionsDocument,
ReqType,
} from "../../api/generated/graphql"
export const createRESTRootUserCollection = (title: string) =>
runMutation<
CreateRestRootUserCollectionMutation,
CreateRestRootUserCollectionMutationVariables,
""
>(CreateRestRootUserCollectionDocument, {
title,
})()
export const createGQLRootUserCollection = (title: string) =>
runMutation<
CreateGqlRootUserCollectionMutation,
CreateGqlRootUserCollectionMutationVariables,
""
>(CreateGqlRootUserCollectionDocument, {
title,
})()
export const createRESTUserRequest = (
title: string,
request: string,
collectionID: string
) =>
runMutation<
CreateRestUserRequestMutation,
CreateRestUserRequestMutationVariables,
""
>(CreateRestUserRequestDocument, {
title,
request,
collectionID,
})()
export const createGQLUserRequest = (
title: string,
request: string,
collectionID: string
) =>
runMutation<
CreateGqlUserRequestMutation,
CreateGqlUserRequestMutationVariables,
""
>(CreateGqlUserRequestDocument, {
title,
request,
collectionID,
})()
export const createRESTChildUserCollection = (
title: string,
parentUserCollectionID: string
) =>
runMutation<
CreateRestChildUserCollectionMutation,
CreateRestChildUserCollectionMutationVariables,
""
>(CreateRestChildUserCollectionDocument, {
title,
parentUserCollectionID,
})()
export const createGQLChildUserCollection = (
title: string,
parentUserCollectionID: string
) =>
runMutation<
CreateGqlChildUserCollectionMutation,
CreateGqlChildUserCollectionMutationVariables,
""
>(CreateGqlChildUserCollectionDocument, {
title,
parentUserCollectionID,
})()
export const deleteUserCollection = (userCollectionID: string) =>
runMutation<
DeleteUserCollectionMutation,
DeleteUserCollectionMutationVariables,
""
>(DeleteUserCollectionDocument, {
userCollectionID,
})()
export const renameUserCollection = (
userCollectionID: string,
newTitle: string
) =>
runMutation<
RenameUserCollectionMutation,
RenameUserCollectionMutationVariables,
""
>(RenameUserCollectionDocument, { userCollectionID, newTitle })()
export const moveUserCollection = (
sourceCollectionID: string,
destinationCollectionID?: string
) =>
runMutation<
MoveUserCollectionMutation,
MoveUserCollectionMutationVariables,
""
>(MoveUserCollectionDocument, {
userCollectionID: sourceCollectionID,
destCollectionID: destinationCollectionID,
})()
export const editUserRequest = (
requestID: string,
title: string,
request: string
) =>
runMutation<
UpdateRestUserRequestMutation,
UpdateRestUserRequestMutationVariables,
""
>(UpdateRestUserRequestDocument, {
id: requestID,
request,
title,
})()
export const editGQLUserRequest = (
requestID: string,
title: string,
request: string
) =>
runMutation<
UpdateGqlUserRequestMutation,
UpdateGqlUserRequestMutationVariables,
""
>(UpdateGqlUserRequestDocument, {
id: requestID,
request,
title,
})()
export const deleteUserRequest = (requestID: string) =>
runMutation<
DeleteUserRequestMutation,
DeleteUserRequestMutationVariables,
""
>(DeleteUserRequestDocument, {
requestID,
})()
export const moveUserRequest = (
sourceCollectionID: string,
destinationCollectionID: string,
requestID: string,
nextRequestID?: string
) =>
runMutation<MoveUserRequestMutation, MoveUserRequestMutationVariables, "">(
MoveUserRequestDocument,
{
sourceCollectionID,
destinationCollectionID,
requestID,
nextRequestID,
}
)()
export const updateUserCollectionOrder = (
collectionID: string,
nextCollectionID?: string
) =>
runMutation<
UpdateUserCollectionOrderMutation,
UpdateUserCollectionOrderMutationVariables,
""
>(UpdateUserCollectionOrderDocument, {
collectionID,
nextCollectionID,
})()
export const getUserRootCollections = () =>
runGQLQuery<
GetUserRootCollectionsQuery,
GetUserRootCollectionsQueryVariables,
""
>({
query: GetUserRootCollectionsDocument,
variables: {},
})
export const getGQLRootUserCollections = () =>
runGQLQuery<
GetGqlRootUserCollectionsQuery,
GetGqlRootUserCollectionsQueryVariables,
""
>({
query: GetGqlRootUserCollectionsDocument,
variables: {},
})
export const exportUserCollectionsToJSON = (
collectionID?: string,
collectionType: ReqType.Rest | ReqType.Gql = ReqType.Rest
) =>
runGQLQuery<
ExportUserCollectionsToJsonQuery,
ExportUserCollectionsToJsonQueryVariables,
""
>({
query: ExportUserCollectionsToJsonDocument,
variables: { collectionID, collectionType },
})
export const runUserCollectionCreatedSubscription = () =>
runGQLSubscription({ query: UserCollectionCreatedDocument, variables: {} })
export const runUserCollectionUpdatedSubscription = () =>
runGQLSubscription({ query: UserCollectionUpdatedDocument, variables: {} })
export const runUserCollectionRemovedSubscription = () =>
runGQLSubscription({ query: UserCollectionRemovedDocument, variables: {} })
export const runUserCollectionMovedSubscription = () =>
runGQLSubscription({ query: UserCollectionMovedDocument, variables: {} })
export const runUserCollectionOrderUpdatedSubscription = () =>
runGQLSubscription({
query: UserCollectionOrderUpdatedDocument,
variables: {},
})
export const runUserRequestCreatedSubscription = () =>
runGQLSubscription({ query: UserRequestCreatedDocument, variables: {} })
export const runUserRequestUpdatedSubscription = () =>
runGQLSubscription({ query: UserRequestUpdatedDocument, variables: {} })
export const runUserRequestMovedSubscription = () =>
runGQLSubscription({ query: UserRequestMovedDocument, variables: {} })
export const runUserRequestDeletedSubscription = () =>
runGQLSubscription({ query: UserRequestDeletedDocument, variables: {} })

View File

@@ -0,0 +1,789 @@
import { authEvents$, def as platformAuth } from "@platform/auth"
import { CollectionsPlatformDef } from "@hoppscotch/common/platform/collections"
import { runDispatchWithOutSyncing } from "../../lib/sync"
import {
exportUserCollectionsToJSON,
runUserCollectionCreatedSubscription,
runUserCollectionMovedSubscription,
runUserCollectionOrderUpdatedSubscription,
runUserCollectionRemovedSubscription,
runUserCollectionUpdatedSubscription,
runUserRequestCreatedSubscription,
runUserRequestDeletedSubscription,
runUserRequestMovedSubscription,
runUserRequestUpdatedSubscription,
} from "./collections.api"
import { collectionsSyncer, getStoreByCollectionType } from "./collections.sync"
import * as E from "fp-ts/Either"
import {
addRESTCollection,
setRESTCollections,
editRESTCollection,
removeRESTCollection,
moveRESTFolder,
updateRESTCollectionOrder,
saveRESTRequestAs,
navigateToFolderWithIndexPath,
editRESTRequest,
removeRESTRequest,
moveRESTRequest,
updateRESTRequestOrder,
addRESTFolder,
editRESTFolder,
removeRESTFolder,
addGraphqlFolder,
addGraphqlCollection,
editGraphqlFolder,
editGraphqlCollection,
removeGraphqlFolder,
removeGraphqlCollection,
saveGraphqlRequestAs,
editGraphqlRequest,
moveGraphqlRequest,
removeGraphqlRequest,
setGraphqlCollections,
restCollectionStore,
} from "@hoppscotch/common/newstore/collections"
import { runGQLSubscription } from "@hoppscotch/common/helpers/backend/GQLClient"
import {
HoppCollection,
HoppGQLRequest,
HoppRESTRequest,
} from "@hoppscotch/data"
import { gqlCollectionsSyncer } from "./gqlCollections.sync"
import { ReqType } from "../../api/generated/graphql"
function initCollectionsSync() {
const currentUser$ = platformAuth.getCurrentUserStream()
collectionsSyncer.startStoreSync()
collectionsSyncer.setupSubscriptions(setupSubscriptions)
gqlCollectionsSyncer.startStoreSync()
loadUserCollections("REST")
loadUserCollections("GQL")
// TODO: test & make sure the auth thing is working properly
currentUser$.subscribe(async (user) => {
if (user) {
loadUserCollections("REST")
loadUserCollections("GQL")
}
})
authEvents$.subscribe((event) => {
if (event.event == "login" || event.event == "token_refresh") {
collectionsSyncer.startListeningToSubscriptions()
}
if (event.event == "logout") {
collectionsSyncer.stopListeningToSubscriptions()
}
})
}
type ExportedUserCollectionREST = {
id?: string
folders: ExportedUserCollectionREST[]
requests: Array<HoppRESTRequest & { id: string }>
name: string
}
type ExportedUserCollectionGQL = {
id?: string
folders: ExportedUserCollectionGQL[]
requests: Array<HoppGQLRequest & { id: string }>
name: string
}
function exportedCollectionToHoppCollection(
collection: ExportedUserCollectionREST | ExportedUserCollectionGQL,
collectionType: "REST" | "GQL"
): HoppCollection<HoppRESTRequest | HoppGQLRequest> {
if (collectionType == "REST") {
const restCollection = collection as ExportedUserCollectionREST
return {
id: restCollection.id,
v: 1,
name: restCollection.name,
folders: restCollection.folders.map((folder) =>
exportedCollectionToHoppCollection(folder, collectionType)
),
requests: restCollection.requests.map(
({
id,
v,
auth,
body,
endpoint,
headers,
method,
name,
params,
preRequestScript,
testScript,
}) => ({
id,
v,
auth,
body,
endpoint,
headers,
method,
name,
params,
preRequestScript,
testScript,
})
),
}
} else {
const gqlCollection = collection as ExportedUserCollectionGQL
return {
id: gqlCollection.id,
v: 1,
name: gqlCollection.name,
folders: gqlCollection.folders.map((folder) =>
exportedCollectionToHoppCollection(folder, collectionType)
),
requests: gqlCollection.requests.map(
({ v, auth, headers, name, id }) => ({
id,
v,
auth,
headers,
name,
})
) as HoppGQLRequest[],
}
}
}
async function loadUserCollections(collectionType: "REST" | "GQL") {
const res = await exportUserCollectionsToJSON(
undefined,
collectionType == "REST" ? ReqType.Rest : ReqType.Gql
)
if (E.isRight(res)) {
const collectionsJSONString =
res.right.exportUserCollectionsToJSON.exportedCollection
const exportedCollections = (
JSON.parse(collectionsJSONString) as Array<
ExportedUserCollectionGQL | ExportedUserCollectionREST
>
).map((collection) => ({ v: 1, ...collection }))
runDispatchWithOutSyncing(() => {
collectionType == "REST"
? setRESTCollections(
exportedCollections.map(
(collection) =>
exportedCollectionToHoppCollection(
collection,
"REST"
) as HoppCollection<HoppRESTRequest>
)
)
: setGraphqlCollections(
exportedCollections.map(
(collection) =>
exportedCollectionToHoppCollection(
collection,
"GQL"
) as HoppCollection<HoppGQLRequest>
)
)
})
}
}
function setupSubscriptions() {
let subs: ReturnType<typeof runGQLSubscription>[1][] = []
const userCollectionCreatedSub = setupUserCollectionCreatedSubscription()
const userCollectionUpdatedSub = setupUserCollectionUpdatedSubscription()
const userCollectionRemovedSub = setupUserCollectionRemovedSubscription()
const userCollectionMovedSub = setupUserCollectionMovedSubscription()
const userCollectionOrderUpdatedSub =
setupUserCollectionOrderUpdatedSubscription()
const userRequestCreatedSub = setupUserRequestCreatedSubscription()
const userRequestUpdatedSub = setupUserRequestUpdatedSubscription()
const userRequestDeletedSub = setupUserRequestDeletedSubscription()
const userRequestMovedSub = setupUserRequestMovedSubscription()
subs = [
userCollectionCreatedSub,
userCollectionUpdatedSub,
userCollectionRemovedSub,
userCollectionMovedSub,
userCollectionOrderUpdatedSub,
userRequestCreatedSub,
userRequestUpdatedSub,
userRequestDeletedSub,
userRequestMovedSub,
]
return () => {
subs.forEach((sub) => sub.unsubscribe())
}
}
function setupUserCollectionCreatedSubscription() {
const [userCollectionCreated$, userCollectionCreatedSub] =
runUserCollectionCreatedSubscription()
userCollectionCreated$.subscribe((res) => {
if (E.isRight(res)) {
const collectionType = res.right.userCollectionCreated.type
const { collectionStore } = getStoreByCollectionType(collectionType)
const userCollectionBackendID = res.right.userCollectionCreated.id
const parentCollectionID = res.right.userCollectionCreated.parent?.id
const userCollectionLocalID = getCollectionPathFromCollectionID(
userCollectionBackendID,
collectionStore.value.state
)
// collection already exists in store ( this instance created it )
if (userCollectionLocalID) {
return
}
const parentCollectionPath =
parentCollectionID &&
getCollectionPathFromCollectionID(
parentCollectionID,
collectionStore.value.state
)
// only folders will have parent collection id
if (parentCollectionID && parentCollectionPath) {
runDispatchWithOutSyncing(() => {
collectionType == "GQL"
? addGraphqlFolder(
res.right.userCollectionCreated.title,
parentCollectionPath
)
: addRESTFolder(
res.right.userCollectionCreated.title,
parentCollectionPath
)
const parentCollection = navigateToFolderWithIndexPath(
collectionStore.value.state,
parentCollectionPath
.split("/")
.map((pathIndex) => parseInt(pathIndex))
)
if (parentCollection) {
const folderIndex = parentCollection.folders.length - 1
const addedFolder = parentCollection.folders[folderIndex]
addedFolder.id = userCollectionBackendID
}
})
} else {
// root collections won't have parentCollectionID
runDispatchWithOutSyncing(() => {
collectionType == "GQL"
? addGraphqlCollection({
name: res.right.userCollectionCreated.title,
folders: [],
requests: [],
v: 1,
})
: addRESTCollection({
name: res.right.userCollectionCreated.title,
folders: [],
requests: [],
v: 1,
})
const localIndex = collectionStore.value.state.length - 1
const addedCollection = collectionStore.value.state[localIndex]
addedCollection.id = userCollectionBackendID
})
}
}
})
return userCollectionCreatedSub
}
function setupUserCollectionUpdatedSubscription() {
const [userCollectionUpdated$, userCollectionUpdatedSub] =
runUserCollectionUpdatedSubscription()
userCollectionUpdated$.subscribe((res) => {
if (E.isRight(res)) {
const collectionType = res.right.userCollectionUpdated.type
const { collectionStore } = getStoreByCollectionType(collectionType)
const updatedCollectionBackendID = res.right.userCollectionUpdated.id
const updatedCollectionLocalPath = getCollectionPathFromCollectionID(
updatedCollectionBackendID,
collectionStore.value.state
)
const isFolder =
updatedCollectionLocalPath &&
updatedCollectionLocalPath.split("/").length > 1
// updated collection is a folder
if (isFolder) {
runDispatchWithOutSyncing(() => {
collectionType == "REST"
? editRESTFolder(updatedCollectionLocalPath, {
name: res.right.userCollectionUpdated.title,
})
: editGraphqlFolder(updatedCollectionLocalPath, {
name: res.right.userCollectionUpdated.title,
})
})
}
// updated collection is a root collection
if (updatedCollectionLocalPath && !isFolder) {
runDispatchWithOutSyncing(() => {
collectionType == "REST"
? editRESTCollection(parseInt(updatedCollectionLocalPath), {
name: res.right.userCollectionUpdated.title,
})
: editGraphqlCollection(parseInt(updatedCollectionLocalPath), {
name: res.right.userCollectionUpdated.title,
})
})
}
}
})
return userCollectionUpdatedSub
}
function setupUserCollectionMovedSubscription() {
const [userCollectionMoved$, userCollectionMovedSub] =
runUserCollectionMovedSubscription()
userCollectionMoved$.subscribe((res) => {
if (E.isRight(res)) {
const movedMetadata = res.right.userCollectionMoved
const sourcePath = getCollectionPathFromCollectionID(
movedMetadata.id,
restCollectionStore.value.state
)
let destinationPath: string | undefined
if (movedMetadata.parent?.id) {
destinationPath =
getCollectionPathFromCollectionID(
movedMetadata.parent?.id,
restCollectionStore.value.state
) ?? undefined
}
sourcePath &&
runDispatchWithOutSyncing(() => {
moveRESTFolder(sourcePath, destinationPath ?? null)
})
}
})
return userCollectionMovedSub
}
function setupUserCollectionRemovedSubscription() {
const [userCollectionRemoved$, userCollectionRemovedSub] =
runUserCollectionRemovedSubscription()
userCollectionRemoved$.subscribe((res) => {
if (E.isRight(res)) {
const removedCollectionBackendID = res.right.userCollectionRemoved.id
const collectionType = res.right.userCollectionRemoved.type
const { collectionStore } = getStoreByCollectionType(collectionType)
const removedCollectionLocalPath = getCollectionPathFromCollectionID(
removedCollectionBackendID,
collectionStore.value.state
)
const isFolder =
removedCollectionLocalPath &&
removedCollectionLocalPath.split("/").length > 1
if (removedCollectionLocalPath && isFolder) {
runDispatchWithOutSyncing(() => {
collectionType == "REST"
? removeRESTFolder(removedCollectionLocalPath)
: removeGraphqlFolder(removedCollectionLocalPath)
})
}
if (removedCollectionLocalPath && !isFolder) {
runDispatchWithOutSyncing(() => {
collectionType == "REST"
? removeRESTCollection(parseInt(removedCollectionLocalPath))
: removeGraphqlCollection(parseInt(removedCollectionLocalPath))
})
}
}
})
return userCollectionRemovedSub
}
function setupUserCollectionOrderUpdatedSubscription() {
const [userCollectionOrderUpdated$, userCollectionOrderUpdatedSub] =
runUserCollectionOrderUpdatedSubscription()
userCollectionOrderUpdated$.subscribe((res) => {
if (E.isRight(res)) {
const { userCollection, nextUserCollection } =
res.right.userCollectionOrderUpdated
const sourceCollectionID = userCollection.id
const destinationCollectionID = nextUserCollection?.id
const sourcePath = getCollectionPathFromCollectionID(
sourceCollectionID,
restCollectionStore.value.state
)
let destinationPath: string | null | undefined
if (destinationCollectionID) {
destinationPath = getCollectionPathFromCollectionID(
destinationCollectionID,
restCollectionStore.value.state
)
}
runDispatchWithOutSyncing(() => {
if (sourcePath) {
updateRESTCollectionOrder(sourcePath, destinationPath ?? null)
}
})
}
})
return userCollectionOrderUpdatedSub
}
function setupUserRequestCreatedSubscription() {
const [userRequestCreated$, userRequestCreatedSub] =
runUserRequestCreatedSubscription()
userRequestCreated$.subscribe((res) => {
if (E.isRight(res)) {
const collectionID = res.right.userRequestCreated.collectionID
const request = JSON.parse(res.right.userRequestCreated.request)
const requestID = res.right.userRequestCreated.id
const requestType = res.right.userRequestCreated.type
const { collectionStore } = getStoreByCollectionType(requestType)
const hasAlreadyHappened = getRequestPathFromRequestID(
requestID,
collectionStore.value.state
)
if (!!hasAlreadyHappened) {
return
}
const collectionPath = getCollectionPathFromCollectionID(
collectionID,
collectionStore.value.state
)
if (collectionID && collectionPath) {
runDispatchWithOutSyncing(() => {
requestType == "REST"
? saveRESTRequestAs(collectionPath, request)
: saveGraphqlRequestAs(collectionPath, request)
const target = navigateToFolderWithIndexPath(
collectionStore.value.state,
collectionPath.split("/").map((index) => parseInt(index))
)
const targetRequest = target?.requests[target?.requests.length - 1]
if (targetRequest) {
targetRequest.id = requestID
}
})
}
}
})
return userRequestCreatedSub
}
function setupUserRequestUpdatedSubscription() {
const [userRequestUpdated$, userRequestUpdatedSub] =
runUserRequestUpdatedSubscription()
userRequestUpdated$.subscribe((res) => {
if (E.isRight(res)) {
const requestType = res.right.userRequestUpdated.type
const { collectionStore } = getStoreByCollectionType(requestType)
const requestPath = getRequestPathFromRequestID(
res.right.userRequestUpdated.id,
collectionStore.value.state
)
const collectionPath = requestPath?.collectionPath
const requestIndex = requestPath?.requestIndex
;(requestIndex || requestIndex == 0) &&
collectionPath &&
runDispatchWithOutSyncing(() => {
requestType == "REST"
? editRESTRequest(
collectionPath,
requestIndex,
JSON.parse(res.right.userRequestUpdated.request)
)
: editGraphqlRequest(
collectionPath,
requestIndex,
JSON.parse(res.right.userRequestUpdated.request)
)
})
}
})
return userRequestUpdatedSub
}
function setupUserRequestMovedSubscription() {
const [userRequestMoved$, userRequestMovedSub] =
runUserRequestMovedSubscription()
userRequestMoved$.subscribe((res) => {
if (E.isRight(res)) {
const { request, nextRequest } = res.right.userRequestMoved
const {
collectionID: destinationCollectionID,
id: sourceRequestID,
type: requestType,
} = request
const { collectionStore } = getStoreByCollectionType(requestType)
const sourceRequestPath = getRequestPathFromRequestID(
sourceRequestID,
collectionStore.value.state
)
const destinationCollectionPath = getCollectionPathFromCollectionID(
destinationCollectionID,
collectionStore.value.state
)
const destinationRequestIndex = destinationCollectionPath
? (() => {
const requestsLength = navigateToFolderWithIndexPath(
collectionStore.value.state,
destinationCollectionPath
.split("/")
.map((index) => parseInt(index))
)?.requests.length
return requestsLength || requestsLength == 0
? requestsLength - 1
: undefined
})()
: undefined
// there is no nextRequest, so request is moved
if (
(destinationRequestIndex || destinationRequestIndex == 0) &&
destinationCollectionPath &&
sourceRequestPath &&
!nextRequest
) {
runDispatchWithOutSyncing(() => {
requestType == "REST"
? moveRESTRequest(
sourceRequestPath.collectionPath,
sourceRequestPath.requestIndex,
destinationCollectionPath
)
: moveGraphqlRequest(
sourceRequestPath.collectionPath,
sourceRequestPath.requestIndex,
destinationCollectionPath
)
})
}
// there is nextRequest, so request is reordered
if (
(destinationRequestIndex || destinationRequestIndex == 0) &&
destinationCollectionPath &&
nextRequest &&
// we don't have request reordering for graphql yet
requestType == "REST"
) {
const { collectionID: nextCollectionID, id: nextRequestID } =
nextRequest
const nextCollectionPath =
getCollectionPathFromCollectionID(
nextCollectionID,
collectionStore.value.state
) ?? undefined
const nextRequestIndex = nextCollectionPath
? getRequestIndex(
nextRequestID,
nextCollectionPath,
collectionStore.value.state
)
: undefined
nextRequestIndex &&
nextCollectionPath &&
sourceRequestPath &&
runDispatchWithOutSyncing(() => {
updateRESTRequestOrder(
sourceRequestPath?.requestIndex,
nextRequestIndex,
nextCollectionPath
)
})
}
}
})
return userRequestMovedSub
}
function setupUserRequestDeletedSubscription() {
const [userRequestDeleted$, userRequestDeletedSub] =
runUserRequestDeletedSubscription()
userRequestDeleted$.subscribe((res) => {
if (E.isRight(res)) {
const requestType = res.right.userRequestDeleted.type
const { collectionStore } = getStoreByCollectionType(requestType)
const deletedRequestPath = getRequestPathFromRequestID(
res.right.userRequestDeleted.id,
collectionStore.value.state
)
;(deletedRequestPath?.requestIndex ||
deletedRequestPath?.requestIndex == 0) &&
deletedRequestPath.collectionPath &&
runDispatchWithOutSyncing(() => {
requestType == "REST"
? removeRESTRequest(
deletedRequestPath.collectionPath,
deletedRequestPath.requestIndex
)
: removeGraphqlRequest(
deletedRequestPath.collectionPath,
deletedRequestPath.requestIndex
)
})
}
})
return userRequestDeletedSub
}
export const def: CollectionsPlatformDef = {
initCollectionsSync,
}
function getCollectionPathFromCollectionID(
collectionID: string,
collections: HoppCollection<HoppRESTRequest | HoppGQLRequest>[],
parentPath?: string
): string | null {
for (const collectionIndex in collections) {
if (collections[collectionIndex].id == collectionID) {
return parentPath
? `${parentPath}/${collectionIndex}`
: `${collectionIndex}`
} else {
const collectionPath = getCollectionPathFromCollectionID(
collectionID,
collections[collectionIndex].folders,
parentPath ? `${parentPath}/${collectionIndex}` : `${collectionIndex}`
)
if (collectionPath) return collectionPath
}
}
return null
}
function getRequestPathFromRequestID(
requestID: string,
collections: HoppCollection<HoppRESTRequest | HoppGQLRequest>[],
parentPath?: string
): { collectionPath: string; requestIndex: number } | null {
for (const collectionIndex in collections) {
const requestIndex = collections[collectionIndex].requests.findIndex(
(request) => request.id == requestID
)
if (requestIndex != -1) {
return {
collectionPath: parentPath
? `${parentPath}/${collectionIndex}`
: `${collectionIndex}`,
requestIndex,
}
} else {
const requestPath = getRequestPathFromRequestID(
requestID,
collections[collectionIndex].folders,
parentPath ? `${parentPath}/${collectionIndex}` : `${collectionIndex}`
)
if (requestPath) return requestPath
}
}
return null
}
function getRequestIndex(
requestID: string,
parentCollectionPath: string,
collections: HoppCollection<HoppRESTRequest | HoppGQLRequest>[]
) {
const collection = navigateToFolderWithIndexPath(
collections,
parentCollectionPath?.split("/").map((index) => parseInt(index))
)
const requestIndex = collection?.requests.findIndex(
(request) => request.id == requestID
)
return requestIndex
}

View File

@@ -0,0 +1,543 @@
import {
graphqlCollectionStore,
navigateToFolderWithIndexPath,
removeDuplicateRESTCollectionOrFolder,
restCollectionStore,
} from "@hoppscotch/common/newstore/collections"
import {
getSettingSubject,
settingsStore,
} from "@hoppscotch/common/newstore/settings"
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
import { getSyncInitFunction } from "../../lib/sync"
import { StoreSyncDefinitionOf } from "../../lib/sync"
import { createMapper } from "../../lib/sync/mapper"
import {
createRESTChildUserCollection,
createRESTRootUserCollection,
createRESTUserRequest,
deleteUserCollection,
deleteUserRequest,
editUserRequest,
moveUserCollection,
moveUserRequest,
renameUserCollection,
updateUserCollectionOrder,
} from "./collections.api"
import * as E from "fp-ts/Either"
// restCollectionsMapper uses the collectionPath as the local identifier
export const restCollectionsMapper = createMapper<string, string>()
// restRequestsMapper uses the collectionPath/requestIndex as the local identifier
export const restRequestsMapper = createMapper<string, string>()
// temp implementation untill the backend implements an endpoint that accepts an entire collection
// TODO: use importCollectionsJSON to do this
const recursivelySyncCollections = async (
collection: HoppCollection<HoppRESTRequest>,
collectionPath: string,
parentUserCollectionID?: string
) => {
let parentCollectionID = parentUserCollectionID
// if parentUserCollectionID does not exist, create the collection as a root collection
if (!parentUserCollectionID) {
const res = await createRESTRootUserCollection(collection.name)
if (E.isRight(res)) {
parentCollectionID = res.right.createRESTRootUserCollection.id
collection.id = parentCollectionID
removeDuplicateRESTCollectionOrFolder(parentCollectionID, collectionPath)
} else {
parentCollectionID = undefined
}
} else {
// if parentUserCollectionID exists, create the collection as a child collection
const res = await createRESTChildUserCollection(
collection.name,
parentUserCollectionID
)
if (E.isRight(res)) {
const childCollectionId = res.right.createRESTChildUserCollection.id
collection.id = childCollectionId
removeDuplicateRESTCollectionOrFolder(
childCollectionId,
`${collectionPath}`
)
}
}
// create the requests
if (parentCollectionID) {
collection.requests.forEach(async (request) => {
const res =
parentCollectionID &&
(await createRESTUserRequest(
request.name,
JSON.stringify(request),
parentCollectionID
))
if (res && E.isRight(res)) {
const requestId = res.right.createRESTUserRequest.id
request.id = requestId
}
})
}
// create the folders aka child collections
if (parentCollectionID)
collection.folders.forEach(async (folder, index) => {
recursivelySyncCollections(
folder,
`${collectionPath}/${index}`,
parentCollectionID
)
})
}
// TODO: generalize this
// TODO: ask backend to send enough info on the subscription to not need this
export const collectionReorderOrMovingOperations: {
sourceCollectionID: string
destinationCollectionID?: string
reorderOperation: {
fromPath: string
toPath?: string
}
}[] = []
type OperationStatus = "pending" | "completed"
type OperationCollectionRemoved = {
type: "COLLECTION_REMOVED"
collectionBackendID: string
status: OperationStatus
}
export const restCollectionsOperations: Array<OperationCollectionRemoved> = []
export const storeSyncDefinition: StoreSyncDefinitionOf<
typeof restCollectionStore
> = {
appendCollections({ entries }) {
let indexStart = restCollectionStore.value.state.length - entries.length
entries.forEach((collection) => {
recursivelySyncCollections(collection, `${indexStart}`)
indexStart++
})
},
async addCollection({ collection }) {
const lastCreatedCollectionIndex =
restCollectionStore.value.state.length - 1
recursivelySyncCollections(collection, `${lastCreatedCollectionIndex}`)
},
async removeCollection({ collectionID }) {
if (collectionID) {
await deleteUserCollection(collectionID)
}
},
editCollection({ partialCollection: collection, collectionIndex }) {
const collectionID = navigateToFolderWithIndexPath(
restCollectionStore.value.state,
[collectionIndex]
)?.id
if (collectionID && collection.name) {
renameUserCollection(collectionID, collection.name)
}
},
async addFolder({ name, path }) {
const parentCollection = navigateToFolderWithIndexPath(
restCollectionStore.value.state,
path.split("/").map((index) => parseInt(index))
)
const parentCollectionBackendID = parentCollection?.id
if (parentCollectionBackendID) {
const foldersLength = parentCollection.folders.length
const res = await createRESTChildUserCollection(
name,
parentCollectionBackendID
)
if (E.isRight(res)) {
const { id } = res.right.createRESTChildUserCollection
if (foldersLength) {
parentCollection.folders[foldersLength - 1].id = id
removeDuplicateRESTCollectionOrFolder(
id,
`${path}/${foldersLength - 1}`
)
}
}
}
},
editFolder({ folder, path }) {
const folderID = navigateToFolderWithIndexPath(
restCollectionStore.value.state,
path.split("/").map((index) => parseInt(index))
)?.id
const folderName = folder.name
if (folderID && folderName) {
renameUserCollection(folderID, folderName)
}
},
async removeFolder({ folderID }) {
if (folderID) {
await deleteUserCollection(folderID)
}
},
async moveFolder({ destinationPath, path }) {
const { newSourcePath, newDestinationPath } = getPathsAfterMoving(
path,
destinationPath ?? undefined
)
if (newSourcePath) {
const sourceCollectionID = navigateToFolderWithIndexPath(
restCollectionStore.value.state,
newSourcePath.split("/").map((index) => parseInt(index))
)?.id
const destinationCollectionID = destinationPath
? newDestinationPath &&
navigateToFolderWithIndexPath(
restCollectionStore.value.state,
newDestinationPath.split("/").map((index) => parseInt(index))
)?.id
: undefined
if (sourceCollectionID) {
await moveUserCollection(sourceCollectionID, destinationCollectionID)
}
}
},
editRequest({ path, requestIndex, requestNew }) {
const request = navigateToFolderWithIndexPath(
restCollectionStore.value.state,
path.split("/").map((index) => parseInt(index))
)?.requests[requestIndex]
const requestBackendID = request?.id
if (requestBackendID) {
editUserRequest(
requestBackendID,
(requestNew as HoppRESTRequest).name,
JSON.stringify(requestNew)
)
}
},
async saveRequestAs({ path, request }) {
const folder = navigateToFolderWithIndexPath(
restCollectionStore.value.state,
path.split("/").map((index) => parseInt(index))
)
const parentCollectionBackendID = folder?.id
if (parentCollectionBackendID) {
const newRequest = folder.requests[folder.requests.length - 1]
const res = await createRESTUserRequest(
(request as HoppRESTRequest).name,
JSON.stringify(request),
parentCollectionBackendID
)
if (E.isRight(res)) {
const { id } = res.right.createRESTUserRequest
newRequest.id = id
removeDuplicateRESTCollectionOrFolder(
id,
`${path}/${folder.requests.length - 1}`,
"request"
)
}
}
},
async removeRequest({ requestID }) {
if (requestID) {
await deleteUserRequest(requestID)
}
},
moveRequest({ destinationPath, path, requestIndex }) {
moveOrReorderRequests(requestIndex, path, destinationPath)
},
updateRequestOrder({
destinationCollectionPath,
destinationRequestIndex,
requestIndex,
}) {
/**
* currently the FE implementation only supports reordering requests between the same collection,
* so destinationCollectionPath and sourceCollectionPath will be same
*/
moveOrReorderRequests(
requestIndex,
destinationCollectionPath,
destinationCollectionPath,
destinationRequestIndex ?? undefined
)
},
async updateCollectionOrder({
collectionIndex: collectionPath,
destinationCollectionIndex: destinationCollectionPath,
}) {
const collections = restCollectionStore.value.state
const sourcePathIndexes = getParentPathIndexesFromPath(collectionPath)
const sourceCollectionIndex = getCollectionIndexFromPath(collectionPath)
const destinationCollectionIndex = !!destinationCollectionPath
? getCollectionIndexFromPath(destinationCollectionPath)
: undefined
let updatedCollectionIndexs:
| [newSourceIndex: number, newDestinationIndex: number | undefined]
| undefined
if (
(sourceCollectionIndex || sourceCollectionIndex == 0) &&
(destinationCollectionIndex || destinationCollectionIndex == 0)
) {
updatedCollectionIndexs = getIndexesAfterReorder(
sourceCollectionIndex,
destinationCollectionIndex
)
} else if (sourceCollectionIndex || sourceCollectionIndex == 0) {
if (sourcePathIndexes.length == 0) {
// we're reordering root collections
updatedCollectionIndexs = [collections.length - 1, undefined]
} else {
const sourceCollection = navigateToFolderWithIndexPath(collections, [
...sourcePathIndexes,
])
if (sourceCollection && sourceCollection.folders.length > 0) {
updatedCollectionIndexs = [
sourceCollection.folders.length - 1,
undefined,
]
}
}
}
const sourceCollectionID =
updatedCollectionIndexs &&
navigateToFolderWithIndexPath(collections, [
...sourcePathIndexes,
updatedCollectionIndexs[0],
])?.id
const destinationCollectionID =
updatedCollectionIndexs &&
(updatedCollectionIndexs[1] || updatedCollectionIndexs[1] == 0)
? navigateToFolderWithIndexPath(collections, [
...sourcePathIndexes,
updatedCollectionIndexs[1],
])?.id
: undefined
if (sourceCollectionID) {
await updateUserCollectionOrder(
sourceCollectionID,
destinationCollectionID
)
}
},
}
export const collectionsSyncer = getSyncInitFunction(
restCollectionStore,
storeSyncDefinition,
() => settingsStore.value.syncCollections,
getSettingSubject("syncCollections")
)
export async function moveOrReorderRequests(
requestIndex: number,
path: string,
destinationPath: string,
nextRequestIndex?: number,
requestType: "REST" | "GQL" = "REST"
) {
const { collectionStore } = getStoreByCollectionType(requestType)
const sourceCollectionBackendID = navigateToFolderWithIndexPath(
collectionStore.value.state,
path.split("/").map((index) => parseInt(index))
)?.id
const destinationCollection = navigateToFolderWithIndexPath(
collectionStore.value.state,
destinationPath.split("/").map((index) => parseInt(index))
)
const destinationCollectionBackendID = destinationCollection?.id
let requestBackendID: string | undefined
let nextRequestBackendID: string | undefined
// we only need this for reordering requests, not for moving requests
if (nextRequestIndex) {
// reordering
const [newRequestIndex, newDestinationIndex] = getIndexesAfterReorder(
requestIndex,
nextRequestIndex
)
requestBackendID =
destinationCollection?.requests[newRequestIndex]?.id ?? undefined
nextRequestBackendID =
destinationCollection?.requests[newDestinationIndex]?.id ?? undefined
} else {
// moving
const requests = destinationCollection?.requests
requestBackendID =
requests && requests.length > 0
? requests[requests.length - 1]?.id
: undefined
}
if (
sourceCollectionBackendID &&
destinationCollectionBackendID &&
requestBackendID
) {
await moveUserRequest(
sourceCollectionBackendID,
destinationCollectionBackendID,
requestBackendID,
nextRequestBackendID
)
}
}
function getParentPathIndexesFromPath(path: string) {
const indexes = path.split("/")
indexes.pop()
return indexes.map((index) => parseInt(index))
}
export function getCollectionIndexFromPath(collectionPath: string) {
const sourceCollectionIndexString = collectionPath.split("/").pop()
const sourceCollectionIndex = sourceCollectionIndexString
? parseInt(sourceCollectionIndexString)
: undefined
return sourceCollectionIndex
}
/**
* the sync function is called after the reordering has happened on the store
* because of this we need to find the new source and destination indexes after the reordering
*/
function getIndexesAfterReorder(
oldSourceIndex: number,
oldDestinationIndex: number
): [newSourceIndex: number, newDestinationIndex: number] {
// Source Becomes Destination -1
// Destination Remains Same
if (oldSourceIndex < oldDestinationIndex) {
return [oldDestinationIndex - 1, oldDestinationIndex]
}
// Source Becomes The Destination
// Destintion Becomes Source + 1
if (oldSourceIndex > oldDestinationIndex) {
return [oldDestinationIndex, oldDestinationIndex + 1]
}
throw new Error("Source and Destination are the same")
}
/**
* the sync function is called after moving a folder has happened on the store,
* because of this the source index given to the sync function is not the live one
* we need to find the new source index after the moving
*/
function getPathsAfterMoving(sourcePath: string, destinationPath?: string) {
if (!destinationPath) {
return {
newSourcePath: `${restCollectionStore.value.state.length - 1}`,
newDestinationPath: destinationPath,
}
}
const sourceParentPath = getParentPathFromPath(sourcePath)
const destinationParentPath = getParentPathFromPath(destinationPath)
const isSameParentPath = sourceParentPath === destinationParentPath
let newDestinationPath: string
if (isSameParentPath) {
const sourceIndex = getCollectionIndexFromPath(sourcePath)
const destinationIndex = getCollectionIndexFromPath(destinationPath)
if (
(sourceIndex || sourceIndex == 0) &&
(destinationIndex || destinationIndex == 0) &&
sourceIndex < destinationIndex
) {
newDestinationPath = destinationParentPath
? `${destinationParentPath}/${destinationIndex - 1}`
: `${destinationIndex - 1}`
} else {
newDestinationPath = destinationPath
}
} else {
newDestinationPath = destinationPath
}
const destinationFolder = navigateToFolderWithIndexPath(
restCollectionStore.value.state,
newDestinationPath.split("/").map((index) => parseInt(index))
)
const newSourcePath = destinationFolder
? `${newDestinationPath}/${destinationFolder?.folders.length - 1}`
: undefined
return {
newSourcePath,
newDestinationPath,
}
}
function getParentPathFromPath(path: string | undefined) {
const indexes = path ? path.split("/") : []
indexes.pop()
return indexes.join("/")
}
export function getStoreByCollectionType(type: "GQL" | "REST") {
const isGQL = type == "GQL"
const collectionStore = isGQL ? graphqlCollectionStore : restCollectionStore
return { collectionStore }
}

View File

@@ -0,0 +1,269 @@
import {
graphqlCollectionStore,
navigateToFolderWithIndexPath,
removeDuplicateGraphqlCollectionOrFolder,
} from "@hoppscotch/common/newstore/collections"
import {
getSettingSubject,
settingsStore,
} from "@hoppscotch/common/newstore/settings"
import { HoppCollection, HoppRESTRequest } from "@hoppscotch/data"
import { getSyncInitFunction } from "../../lib/sync"
import { StoreSyncDefinitionOf } from "../../lib/sync"
import { createMapper } from "../../lib/sync/mapper"
import {
createGQLChildUserCollection,
createGQLRootUserCollection,
createGQLUserRequest,
deleteUserCollection,
deleteUserRequest,
editGQLUserRequest,
renameUserCollection,
} from "./collections.api"
import * as E from "fp-ts/Either"
import { moveOrReorderRequests } from "./collections.sync"
// gqlCollectionsMapper uses the collectionPath as the local identifier
export const gqlCollectionsMapper = createMapper<string, string>()
// gqlRequestsMapper uses the collectionPath/requestIndex as the local identifier
export const gqlRequestsMapper = createMapper<string, string>()
// temp implementation untill the backend implements an endpoint that accepts an entire collection
// TODO: use importCollectionsJSON to do this
const recursivelySyncCollections = async (
collection: HoppCollection<HoppRESTRequest>,
collectionPath: string,
parentUserCollectionID?: string
) => {
let parentCollectionID = parentUserCollectionID
// if parentUserCollectionID does not exist, create the collection as a root collection
if (!parentUserCollectionID) {
const res = await createGQLRootUserCollection(collection.name)
if (E.isRight(res)) {
parentCollectionID = res.right.createGQLRootUserCollection.id
collection.id = parentCollectionID
removeDuplicateGraphqlCollectionOrFolder(
parentCollectionID,
collectionPath
)
} else {
parentCollectionID = undefined
}
} else {
// if parentUserCollectionID exists, create the collection as a child collection
const res = await createGQLChildUserCollection(
collection.name,
parentUserCollectionID
)
if (E.isRight(res)) {
const childCollectionId = res.right.createGQLChildUserCollection.id
collection.id = childCollectionId
removeDuplicateGraphqlCollectionOrFolder(
childCollectionId,
`${collectionPath}`
)
}
}
// create the requests
if (parentCollectionID) {
collection.requests.forEach(async (request) => {
const res =
parentCollectionID &&
(await createGQLUserRequest(
request.name,
JSON.stringify(request),
parentCollectionID
))
if (res && E.isRight(res)) {
const requestId = res.right.createGQLUserRequest.id
request.id = requestId
}
})
}
// create the folders aka child collections
if (parentCollectionID)
collection.folders.forEach(async (folder, index) => {
recursivelySyncCollections(
folder,
`${collectionPath}/${index}`,
parentCollectionID
)
})
}
// TODO: generalize this
// TODO: ask backend to send enough info on the subscription to not need this
export const collectionReorderOrMovingOperations: {
sourceCollectionID: string
destinationCollectionID?: string
reorderOperation: {
fromPath: string
toPath?: string
}
}[] = []
type OperationStatus = "pending" | "completed"
type OperationCollectionRemoved = {
type: "COLLECTION_REMOVED"
collectionBackendID: string
status: OperationStatus
}
export const gqlCollectionsOperations: Array<OperationCollectionRemoved> = []
export const storeSyncDefinition: StoreSyncDefinitionOf<
typeof graphqlCollectionStore
> = {
appendCollections({ entries }) {
let indexStart = graphqlCollectionStore.value.state.length - entries.length
entries.forEach((collection) => {
recursivelySyncCollections(collection, `${indexStart}`)
indexStart++
})
},
async addCollection({ collection }) {
const lastCreatedCollectionIndex =
graphqlCollectionStore.value.state.length - 1
await recursivelySyncCollections(
collection,
`${lastCreatedCollectionIndex}`
)
},
async removeCollection({ collectionID }) {
if (collectionID) {
await deleteUserCollection(collectionID)
}
},
editCollection({ collection, collectionIndex }) {
const collectionID = navigateToFolderWithIndexPath(
graphqlCollectionStore.value.state,
[collectionIndex]
)?.id
if (collectionID && collection.name) {
renameUserCollection(collectionID, collection.name)
}
},
async addFolder({ name, path }) {
const parentCollection = navigateToFolderWithIndexPath(
graphqlCollectionStore.value.state,
path.split("/").map((index) => parseInt(index))
)
const parentCollectionBackendID = parentCollection?.id
if (parentCollectionBackendID) {
const foldersLength = parentCollection.folders.length
const res = await createGQLChildUserCollection(
name,
parentCollectionBackendID
)
if (E.isRight(res)) {
const { id } = res.right.createGQLChildUserCollection
if (foldersLength) {
parentCollection.folders[foldersLength - 1].id = id
removeDuplicateGraphqlCollectionOrFolder(
id,
`${path}/${foldersLength - 1}`
)
}
}
}
},
editFolder({ folder, path }) {
const folderBackendId = navigateToFolderWithIndexPath(
graphqlCollectionStore.value.state,
path.split("/").map((index) => parseInt(index))
)?.id
if (folderBackendId && folder.name) {
renameUserCollection(folderBackendId, folder.name)
}
},
async removeFolder({ folderID }) {
if (folderID) {
await deleteUserCollection(folderID)
}
},
editRequest({ path, requestIndex, requestNew }) {
const request = navigateToFolderWithIndexPath(
graphqlCollectionStore.value.state,
path.split("/").map((index) => parseInt(index))
)?.requests[requestIndex]
const requestBackendID = request?.id
if (requestBackendID) {
editGQLUserRequest(
requestBackendID,
(requestNew as HoppRESTRequest).name,
JSON.stringify(requestNew)
)
}
},
async saveRequestAs({ path, request }) {
const folder = navigateToFolderWithIndexPath(
graphqlCollectionStore.value.state,
path.split("/").map((index) => parseInt(index))
)
const parentCollectionBackendID = folder?.id
if (parentCollectionBackendID) {
const newRequest = folder.requests[folder.requests.length - 1]
const res = await createGQLUserRequest(
(request as HoppRESTRequest).name,
JSON.stringify(request),
parentCollectionBackendID
)
if (E.isRight(res)) {
const { id } = res.right.createGQLUserRequest
newRequest.id = id
removeDuplicateGraphqlCollectionOrFolder(
id,
`${path}/${folder.requests.length - 1}`,
"request"
)
}
}
},
async removeRequest({ requestID }) {
if (requestID) {
await deleteUserRequest(requestID)
}
},
moveRequest({ destinationPath, path, requestIndex }) {
moveOrReorderRequests(requestIndex, path, destinationPath, undefined, "GQL")
},
}
export const gqlCollectionsSyncer = getSyncInitFunction(
graphqlCollectionStore,
storeSyncDefinition,
() => settingsStore.value.syncCollections,
getSettingSubject("syncCollections")
)

View File

@@ -0,0 +1,117 @@
import {
runMutation,
runGQLQuery,
runGQLSubscription,
} from "@hoppscotch/common/helpers/backend/GQLClient"
import {
CreateUserEnvironmentDocument,
CreateUserEnvironmentMutation,
CreateUserEnvironmentMutationVariables,
UpdateUserEnvironmentMutation,
UpdateUserEnvironmentMutationVariables,
UpdateUserEnvironmentDocument,
DeleteUserEnvironmentMutation,
DeleteUserEnvironmentMutationVariables,
DeleteUserEnvironmentDocument,
ClearGlobalEnvironmentsMutation,
ClearGlobalEnvironmentsMutationVariables,
ClearGlobalEnvironmentsDocument,
CreateUserGlobalEnvironmentMutation,
CreateUserGlobalEnvironmentMutationVariables,
CreateUserGlobalEnvironmentDocument,
GetGlobalEnvironmentsDocument,
GetGlobalEnvironmentsQueryVariables,
GetGlobalEnvironmentsQuery,
GetUserEnvironmentsDocument,
UserEnvironmentCreatedDocument,
UserEnvironmentUpdatedDocument,
UserEnvironmentDeletedDocument,
} from "./../../api/generated/graphql"
import { Environment } from "@hoppscotch/data"
export const createUserEnvironment = (name: string, variables: string) =>
runMutation<
CreateUserEnvironmentMutation,
CreateUserEnvironmentMutationVariables,
""
>(CreateUserEnvironmentDocument, {
name,
variables,
})()
export const updateUserEnvironment = (
id: string,
{ name, variables }: Environment
) =>
runMutation<
UpdateUserEnvironmentMutation,
UpdateUserEnvironmentMutationVariables,
""
>(UpdateUserEnvironmentDocument, {
id,
name,
variables: JSON.stringify(variables),
})
export const deleteUserEnvironment = (id: string) =>
runMutation<
DeleteUserEnvironmentMutation,
DeleteUserEnvironmentMutationVariables,
""
>(DeleteUserEnvironmentDocument, {
id,
})
export const clearGlobalEnvironmentVariables = (id: string) =>
runMutation<
ClearGlobalEnvironmentsMutation,
ClearGlobalEnvironmentsMutationVariables,
""
>(ClearGlobalEnvironmentsDocument, {
id,
})()
export const getUserEnvironments = () =>
runGQLQuery({
query: GetUserEnvironmentsDocument,
variables: {},
})
export const getGlobalEnvironments = () =>
runGQLQuery<
GetGlobalEnvironmentsQuery,
GetGlobalEnvironmentsQueryVariables,
"user_environment/user_env_does_not_exists"
>({
query: GetGlobalEnvironmentsDocument,
variables: {},
})
export const createUserGlobalEnvironment = (variables: string) =>
runMutation<
CreateUserGlobalEnvironmentMutation,
CreateUserGlobalEnvironmentMutationVariables,
""
>(CreateUserGlobalEnvironmentDocument, {
variables,
})()
export const runUserEnvironmentCreatedSubscription = () =>
runGQLSubscription({
query: UserEnvironmentCreatedDocument,
variables: {},
})
export const runUserEnvironmentUpdatedSubscription = () =>
runGQLSubscription({
query: UserEnvironmentUpdatedDocument,
variables: {},
})
export const runUserEnvironmentDeletedSubscription = () =>
runGQLSubscription({
query: UserEnvironmentDeletedDocument,
variables: {},
})

View File

@@ -0,0 +1,200 @@
import { authEvents$, def as platformAuth } from "@platform/auth"
import {
createEnvironment,
deleteEnvironment,
environmentsStore,
getLocalIndexByEnvironmentID,
replaceEnvironments,
setGlobalEnvID,
setGlobalEnvVariables,
updateEnvironment,
} from "@hoppscotch/common/newstore/environments"
import { EnvironmentsPlatformDef } from "@hoppscotch/common/src/platform/environments"
import { runGQLSubscription } from "@hoppscotch/common/helpers/backend/GQLClient"
import { environnmentsSyncer } from "@platform/environments/environments.sync"
import * as E from "fp-ts/Either"
import { runDispatchWithOutSyncing } from "@lib/sync"
import {
createUserGlobalEnvironment,
getGlobalEnvironments,
getUserEnvironments,
runUserEnvironmentCreatedSubscription,
runUserEnvironmentDeletedSubscription,
runUserEnvironmentUpdatedSubscription,
} from "@platform/environments/environments.api"
export function initEnvironmentsSync() {
const currentUser$ = platformAuth.getCurrentUserStream()
environnmentsSyncer.startStoreSync()
environnmentsSyncer.setupSubscriptions(setupSubscriptions)
currentUser$.subscribe(async (user) => {
if (user) {
await loadAllEnvironments()
}
})
authEvents$.subscribe((event) => {
if (event.event == "login" || event.event == "token_refresh") {
environnmentsSyncer.startListeningToSubscriptions()
}
if (event.event == "logout") {
environnmentsSyncer.stopListeningToSubscriptions()
}
})
}
export const def: EnvironmentsPlatformDef = {
initEnvironmentsSync,
}
function setupSubscriptions() {
let subs: ReturnType<typeof runGQLSubscription>[1][] = []
const userEnvironmentCreatedSub = setupUserEnvironmentCreatedSubscription()
const userEnvironmentUpdatedSub = setupUserEnvironmentUpdatedSubscription()
const userEnvironmentDeletedSub = setupUserEnvironmentDeletedSubscription()
subs = [
userEnvironmentCreatedSub,
userEnvironmentUpdatedSub,
userEnvironmentDeletedSub,
]
return () => {
subs.forEach((sub) => sub.unsubscribe())
}
}
async function loadUserEnvironments() {
const res = await getUserEnvironments()
if (E.isRight(res)) {
const environments = res.right.me.environments
if (environments.length > 0) {
runDispatchWithOutSyncing(() => {
replaceEnvironments(
environments.map(({ id, variables, name }) => ({
id,
name,
variables: JSON.parse(variables),
}))
)
})
}
}
}
async function loadGlobalEnvironments() {
const res = await getGlobalEnvironments()
if (E.isRight(res)) {
const globalEnv = res.right.me.globalEnvironments
if (globalEnv) {
runDispatchWithOutSyncing(() => {
setGlobalEnvVariables(JSON.parse(globalEnv.variables))
setGlobalEnvID(globalEnv.id)
})
}
} else if (res.left.error == "user_environment/user_env_does_not_exists") {
const res = await createUserGlobalEnvironment(JSON.stringify([]))
if (E.isRight(res)) {
const backendId = res.right.createUserGlobalEnvironment.id
setGlobalEnvID(backendId)
}
}
}
async function loadAllEnvironments() {
await loadUserEnvironments()
await loadGlobalEnvironments()
}
function setupUserEnvironmentCreatedSubscription() {
const [userEnvironmentCreated$, userEnvironmentCreatedSub] =
runUserEnvironmentCreatedSubscription()
userEnvironmentCreated$.subscribe((res) => {
if (E.isRight(res)) {
const { name, variables, id } = res.right.userEnvironmentCreated
const isAlreadyExisting = environmentsStore.value.environments.some(
(env) => env.id == id
)
if (name && !isAlreadyExisting) {
runDispatchWithOutSyncing(() => {
createEnvironment(name, JSON.parse(variables), id)
})
}
}
})
return userEnvironmentCreatedSub
}
function setupUserEnvironmentUpdatedSubscription() {
const [userEnvironmentUpdated$, userEnvironmentUpdatedSub] =
runUserEnvironmentUpdatedSubscription()
userEnvironmentUpdated$.subscribe((res) => {
if (E.isRight(res)) {
const { name, variables, id, isGlobal } = res.right.userEnvironmentUpdated
// handle the case for global environments
if (isGlobal) {
runDispatchWithOutSyncing(() => {
setGlobalEnvVariables(JSON.parse(variables))
})
} else {
// handle the case for normal environments
const localIndex = environmentsStore.value.environments.findIndex(
(env) => env.id == id
)
if ((localIndex || localIndex == 0) && name) {
runDispatchWithOutSyncing(() => {
updateEnvironment(localIndex, {
id,
name,
variables: JSON.parse(variables),
})
})
}
}
}
})
return userEnvironmentUpdatedSub
}
function setupUserEnvironmentDeletedSubscription() {
const [userEnvironmentDeleted$, userEnvironmentDeletedSub] =
runUserEnvironmentDeletedSubscription()
userEnvironmentDeleted$.subscribe((res) => {
if (E.isRight(res)) {
const { id } = res.right.userEnvironmentDeleted
// TODO: move getLocalIndexByID to a getter in the environmentsStore
const localIndex = getLocalIndexByEnvironmentID(id)
if (localIndex || localIndex === 0) {
runDispatchWithOutSyncing(() => {
deleteEnvironment(localIndex)
})
}
}
})
return userEnvironmentDeletedSub
}

View File

@@ -0,0 +1,117 @@
import {
environmentsStore,
getGlobalVariableID,
removeDuplicateEntry,
} from "@hoppscotch/common/newstore/environments"
import {
getSettingSubject,
settingsStore,
} from "@hoppscotch/common/newstore/settings"
import { getSyncInitFunction } from "../../lib/sync"
import * as E from "fp-ts/Either"
import { StoreSyncDefinitionOf } from "../../lib/sync"
import { createMapper } from "../../lib/sync/mapper"
import {
clearGlobalEnvironmentVariables,
createUserEnvironment,
deleteUserEnvironment,
updateUserEnvironment,
} from "./environments.api"
export const environmentsMapper = createMapper<number, string>()
export const globalEnvironmentMapper = createMapper<number, string>()
export const storeSyncDefinition: StoreSyncDefinitionOf<
typeof environmentsStore
> = {
async createEnvironment({ name, variables }) {
const lastCreatedEnvIndex = environmentsStore.value.environments.length - 1
const res = await createUserEnvironment(name, JSON.stringify(variables))
if (E.isRight(res)) {
const id = res.right.createUserEnvironment.id
environmentsStore.value.environments[lastCreatedEnvIndex].id = id
removeDuplicateEntry(id)
}
},
async appendEnvironments({ envs }) {
const appendListLength = envs.length
let appendStart =
environmentsStore.value.environments.length - appendListLength - 1
envs.forEach((env) => {
const envId = ++appendStart
;(async function () {
const res = await createUserEnvironment(
env.name,
JSON.stringify(env.variables)
)
if (E.isRight(res)) {
const id = res.right.createUserEnvironment.id
environmentsStore.value.environments[envId].id = id
removeDuplicateEntry(id)
}
})()
})
},
async duplicateEnvironment({ envIndex }) {
const environmentToDuplicate = environmentsStore.value.environments.find(
(_, index) => index === envIndex
)
const lastCreatedEnvIndex = environmentsStore.value.environments.length - 1
if (environmentToDuplicate) {
const res = await createUserEnvironment(
`${environmentToDuplicate?.name} - Duplicate`,
JSON.stringify(environmentToDuplicate?.variables)
)
if (E.isRight(res)) {
const id = res.right.createUserEnvironment.id
environmentsStore.value.environments[lastCreatedEnvIndex].id = id
removeDuplicateEntry(id)
}
}
},
updateEnvironment({ envIndex, updatedEnv }) {
const backendId = environmentsStore.value.environments[envIndex].id
if (backendId) {
updateUserEnvironment(backendId, updatedEnv)()
}
},
async deleteEnvironment({ envID }) {
if (envID) {
await deleteUserEnvironment(envID)()
}
},
setGlobalVariables({ entries }) {
const backendId = getGlobalVariableID()
if (backendId) {
updateUserEnvironment(backendId, { name: "", variables: entries })()
}
},
clearGlobalVariables() {
const backendId = getGlobalVariableID()
if (backendId) {
clearGlobalEnvironmentVariables(backendId)
}
},
}
export const environnmentsSyncer = getSyncInitFunction(
environmentsStore,
storeSyncDefinition,
() => settingsStore.value.syncEnvironments,
getSettingSubject("syncEnvironments")
)

View File

@@ -0,0 +1,100 @@
import {
runMutation,
runGQLQuery,
runGQLSubscription,
} from "@hoppscotch/common/helpers/backend/GQLClient"
import {
CreateUserHistoryDocument,
CreateUserHistoryMutation,
CreateUserHistoryMutationVariables,
DeleteAllUserHistoryDocument,
DeleteAllUserHistoryMutation,
DeleteAllUserHistoryMutationVariables,
GetRestUserHistoryDocument,
GetRestUserHistoryQuery,
GetRestUserHistoryQueryVariables,
RemoveRequestFromHistoryDocument,
RemoveRequestFromHistoryMutation,
RemoveRequestFromHistoryMutationVariables,
ReqType,
ToggleHistoryStarStatusDocument,
ToggleHistoryStarStatusMutation,
ToggleHistoryStarStatusMutationVariables,
UserHistoryCreatedDocument,
UserHistoryDeletedDocument,
UserHistoryDeletedManyDocument,
UserHistoryUpdatedDocument,
} from "../../api/generated/graphql"
export const getUserHistoryEntries = () =>
runGQLQuery<GetRestUserHistoryQuery, GetRestUserHistoryQueryVariables, "">({
query: GetRestUserHistoryDocument,
variables: {},
})
export const createUserHistory = (
reqData: string,
resMetadata: string,
reqType: ReqType
) =>
runMutation<
CreateUserHistoryMutation,
CreateUserHistoryMutationVariables,
""
>(CreateUserHistoryDocument, {
reqData,
resMetadata,
reqType,
})()
export const toggleHistoryStarStatus = (id: string) =>
runMutation<
ToggleHistoryStarStatusMutation,
ToggleHistoryStarStatusMutationVariables,
""
>(ToggleHistoryStarStatusDocument, {
id,
})()
export const removeRequestFromHistory = (id: string) =>
runMutation<
RemoveRequestFromHistoryMutation,
RemoveRequestFromHistoryMutationVariables,
""
>(RemoveRequestFromHistoryDocument, {
id,
})()
export const deleteAllUserHistory = (reqType: ReqType) =>
runMutation<
DeleteAllUserHistoryMutation,
DeleteAllUserHistoryMutationVariables,
""
>(DeleteAllUserHistoryDocument, {
reqType,
})()
export const runUserHistoryCreatedSubscription = () =>
runGQLSubscription({
query: UserHistoryCreatedDocument,
variables: {},
})
export const runUserHistoryUpdatedSubscription = () =>
runGQLSubscription({
query: UserHistoryUpdatedDocument,
variables: {},
})
export const runUserHistoryDeletedSubscription = () =>
runGQLSubscription({
query: UserHistoryDeletedDocument,
variables: {},
})
export const runUserHistoryDeletedManySubscription = () =>
runGQLSubscription({
query: UserHistoryDeletedManyDocument,
variables: {},
})

View File

@@ -0,0 +1,261 @@
import { authEvents$, def as platformAuth } from "@platform/auth"
import {
restHistoryStore,
RESTHistoryEntry,
setRESTHistoryEntries,
addRESTHistoryEntry,
toggleRESTHistoryEntryStar,
deleteRESTHistoryEntry,
clearRESTHistory,
setGraphqlHistoryEntries,
GQLHistoryEntry,
addGraphqlHistoryEntry,
toggleGraphqlHistoryEntryStar,
graphqlHistoryStore,
deleteGraphqlHistoryEntry,
clearGraphqlHistory,
} from "@hoppscotch/common/newstore/history"
import { HistoryPlatformDef } from "@hoppscotch/common/platform/history"
import {
getUserHistoryEntries,
runUserHistoryCreatedSubscription,
runUserHistoryDeletedManySubscription,
runUserHistoryDeletedSubscription,
runUserHistoryUpdatedSubscription,
} from "./history.api"
import * as E from "fp-ts/Either"
import { restHistorySyncer, gqlHistorySyncer } from "./history.sync"
import { runGQLSubscription } from "@hoppscotch/common/helpers/backend/GQLClient"
import { runDispatchWithOutSyncing } from "@lib/sync"
import { ReqType } from "../../api/generated/graphql"
function initHistorySync() {
const currentUser$ = platformAuth.getCurrentUserStream()
restHistorySyncer.startStoreSync()
restHistorySyncer.setupSubscriptions(setupSubscriptions)
gqlHistorySyncer.startStoreSync()
loadHistoryEntries()
currentUser$.subscribe(async (user) => {
if (user) {
await loadHistoryEntries()
}
})
authEvents$.subscribe((event) => {
if (event.event == "login" || event.event == "token_refresh") {
restHistorySyncer.startListeningToSubscriptions()
}
if (event.event == "logout") {
restHistorySyncer.stopListeningToSubscriptions()
}
})
}
function setupSubscriptions() {
let subs: ReturnType<typeof runGQLSubscription>[1][] = []
const userHistoryCreatedSub = setupUserHistoryCreatedSubscription()
const userHistoryUpdatedSub = setupUserHistoryUpdatedSubscription()
const userHistoryDeletedSub = setupUserHistoryDeletedSubscription()
const userHistoryDeletedManySub = setupUserHistoryDeletedManySubscription()
subs = [
userHistoryCreatedSub,
userHistoryUpdatedSub,
userHistoryDeletedSub,
userHistoryDeletedManySub,
]
return () => {
subs.forEach((sub) => sub.unsubscribe())
}
}
async function loadHistoryEntries() {
const res = await getUserHistoryEntries()
if (E.isRight(res)) {
const restEntries = res.right.me.RESTHistory
const gqlEntries = res.right.me.GQLHistory
const restHistoryEntries: RESTHistoryEntry[] = restEntries.map((entry) => ({
v: 1,
request: JSON.parse(entry.request),
responseMeta: JSON.parse(entry.responseMetadata),
star: entry.isStarred,
updatedOn: new Date(entry.executedOn),
id: entry.id,
}))
const gqlHistoryEntries: GQLHistoryEntry[] = gqlEntries.map((entry) => ({
v: 1,
request: JSON.parse(entry.request),
response: JSON.parse(entry.responseMetadata),
star: entry.isStarred,
updatedOn: new Date(entry.executedOn),
id: entry.id,
}))
runDispatchWithOutSyncing(() => {
setRESTHistoryEntries(restHistoryEntries)
setGraphqlHistoryEntries(gqlHistoryEntries)
})
}
}
function setupUserHistoryCreatedSubscription() {
const [userHistoryCreated$, userHistoryCreatedSub] =
runUserHistoryCreatedSubscription()
userHistoryCreated$.subscribe((res) => {
if (E.isRight(res)) {
const { id, reqType, request, responseMetadata, isStarred, executedOn } =
res.right.userHistoryCreated
const hasAlreadyBeenAdded =
reqType == ReqType.Rest
? restHistoryStore.value.state.some((entry) => entry.id == id)
: graphqlHistoryStore.value.state.some((entry) => entry.id == id)
!hasAlreadyBeenAdded &&
runDispatchWithOutSyncing(() => {
reqType == ReqType.Rest
? addRESTHistoryEntry({
v: 1,
id,
request: JSON.parse(request),
responseMeta: JSON.parse(responseMetadata),
star: isStarred,
updatedOn: new Date(executedOn),
})
: addGraphqlHistoryEntry({
v: 1,
id,
request: JSON.parse(request),
response: JSON.parse(responseMetadata),
star: isStarred,
updatedOn: new Date(executedOn),
})
})
}
})
return userHistoryCreatedSub
}
// currently the updates are only for toggling the star
function setupUserHistoryUpdatedSubscription() {
const [userHistoryUpdated$, userHistoryUpdatedSub] =
runUserHistoryUpdatedSubscription()
userHistoryUpdated$.subscribe((res) => {
if (E.isRight(res)) {
const { id, executedOn, isStarred, request, responseMetadata, reqType } =
res.right.userHistoryUpdated
if (reqType == ReqType.Rest) {
const updatedRestEntryIndex = restHistoryStore.value.state.findIndex(
(entry) => entry.id == id
)
if (updatedRestEntryIndex != -1) {
runDispatchWithOutSyncing(() => {
toggleRESTHistoryEntryStar({
v: 1,
id,
request: JSON.parse(request),
responseMeta: JSON.parse(responseMetadata),
// because the star will be toggled in the store, we need to pass the opposite value
star: !isStarred,
updatedOn: new Date(executedOn),
})
})
}
}
if (reqType == ReqType.Gql) {
const updatedGQLEntryIndex = graphqlHistoryStore.value.state.findIndex(
(entry) => entry.id == id
)
if (updatedGQLEntryIndex != -1) {
runDispatchWithOutSyncing(() => {
toggleGraphqlHistoryEntryStar({
v: 1,
id,
request: JSON.parse(request),
response: JSON.parse(responseMetadata),
// because the star will be toggled in the store, we need to pass the opposite value
star: !isStarred,
updatedOn: new Date(executedOn),
})
})
}
}
}
})
return userHistoryUpdatedSub
}
function setupUserHistoryDeletedSubscription() {
const [userHistoryDeleted$, userHistoryDeletedSub] =
runUserHistoryDeletedSubscription()
userHistoryDeleted$.subscribe((res) => {
if (E.isRight(res)) {
const { id, reqType } = res.right.userHistoryDeleted
if (reqType == ReqType.Gql) {
const deletedEntry = graphqlHistoryStore.value.state.find(
(entry) => entry.id == id
)
deletedEntry &&
runDispatchWithOutSyncing(() => {
deleteGraphqlHistoryEntry(deletedEntry)
})
}
if (reqType == ReqType.Rest) {
const deletedEntry = restHistoryStore.value.state.find(
(entry) => entry.id == id
)
deletedEntry &&
runDispatchWithOutSyncing(() => {
deleteRESTHistoryEntry(deletedEntry)
})
}
}
})
return userHistoryDeletedSub
}
function setupUserHistoryDeletedManySubscription() {
const [userHistoryDeletedMany$, userHistoryDeletedManySub] =
runUserHistoryDeletedManySubscription()
userHistoryDeletedMany$.subscribe((res) => {
if (E.isRight(res)) {
const { reqType } = res.right.userHistoryDeletedMany
runDispatchWithOutSyncing(() => {
reqType == ReqType.Rest ? clearRESTHistory() : clearGraphqlHistory()
})
}
})
return userHistoryDeletedManySub
}
export const def: HistoryPlatformDef = {
initHistorySync,
}

View File

@@ -0,0 +1,101 @@
import {
graphqlHistoryStore,
removeDuplicateRestHistoryEntry,
removeDuplicateGraphqlHistoryEntry,
restHistoryStore,
} from "@hoppscotch/common/newstore/history"
import {
getSettingSubject,
settingsStore,
} from "@hoppscotch/common/newstore/settings"
import { getSyncInitFunction } from "../../lib/sync"
import * as E from "fp-ts/Either"
import { StoreSyncDefinitionOf } from "../../lib/sync"
import {
createUserHistory,
deleteAllUserHistory,
removeRequestFromHistory,
toggleHistoryStarStatus,
} from "./history.api"
import { ReqType } from "../../api/generated/graphql"
export const restHistoryStoreSyncDefinition: StoreSyncDefinitionOf<
typeof restHistoryStore
> = {
async addEntry({ entry }) {
const res = await createUserHistory(
JSON.stringify(entry.request),
JSON.stringify(entry.responseMeta),
ReqType.Rest
)
if (E.isRight(res)) {
entry.id = res.right.createUserHistory.id
// preventing double insertion from here and subscription
removeDuplicateRestHistoryEntry(entry.id)
}
},
deleteEntry({ entry }) {
if (entry.id) {
removeRequestFromHistory(entry.id)
}
},
toggleStar({ entry }) {
if (entry.id) {
toggleHistoryStarStatus(entry.id)
}
},
clearHistory() {
deleteAllUserHistory(ReqType.Rest)
},
}
export const gqlHistoryStoreSyncDefinition: StoreSyncDefinitionOf<
typeof graphqlHistoryStore
> = {
async addEntry({ entry }) {
const res = await createUserHistory(
JSON.stringify(entry.request),
JSON.stringify(entry.response),
ReqType.Gql
)
if (E.isRight(res)) {
entry.id = res.right.createUserHistory.id
// preventing double insertion from here and subscription
removeDuplicateGraphqlHistoryEntry(entry.id)
}
},
deleteEntry({ entry }) {
if (entry.id) {
removeRequestFromHistory(entry.id)
}
},
toggleStar({ entry }) {
if (entry.id) {
toggleHistoryStarStatus(entry.id)
}
},
clearHistory() {
deleteAllUserHistory(ReqType.Gql)
},
}
export const restHistorySyncer = getSyncInitFunction(
restHistoryStore,
restHistoryStoreSyncDefinition,
() => settingsStore.value.syncHistory,
getSettingSubject("syncHistory")
)
export const gqlHistorySyncer = getSyncInitFunction(
graphqlHistoryStore,
gqlHistoryStoreSyncDefinition,
() => settingsStore.value.syncHistory,
getSettingSubject("syncHistory")
)

View File

@@ -0,0 +1,51 @@
import {
runGQLQuery,
runGQLSubscription,
runMutation,
} from "@hoppscotch/common/helpers/backend/GQLClient"
import {
CreateUserSettingsDocument,
CreateUserSettingsMutation,
CreateUserSettingsMutationVariables,
GetUserSettingsDocument,
GetUserSettingsQuery,
GetUserSettingsQueryVariables,
UpdateUserSettingsDocument,
UpdateUserSettingsMutation,
UpdateUserSettingsMutationVariables,
UserSettingsUpdatedDocument,
} from "../../api/generated/graphql"
export const getUserSettings = () =>
runGQLQuery<
GetUserSettingsQuery,
GetUserSettingsQueryVariables,
"user_settings/not_found"
>({
query: GetUserSettingsDocument,
variables: {},
})
export const createUserSettings = (properties: string) =>
runMutation<
CreateUserSettingsMutation,
CreateUserSettingsMutationVariables,
""
>(CreateUserSettingsDocument, {
properties,
})()
export const updateUserSettings = (properties: string) =>
runMutation<
UpdateUserSettingsMutation,
UpdateUserSettingsMutationVariables,
""
>(UpdateUserSettingsDocument, {
properties,
})()
export const runUserSettingsUpdatedSubscription = () =>
runGQLSubscription({
query: UserSettingsUpdatedDocument,
variables: {},
})

View File

@@ -0,0 +1,88 @@
import { SettingsPlatformDef } from "@hoppscotch/common/platform/settings"
import { settingsSyncer } from "./settings.sync"
import { authEvents$, def as platformAuth } from "@platform/auth"
import {
createUserSettings,
getUserSettings,
runUserSettingsUpdatedSubscription,
} from "./settings.api"
import * as E from "fp-ts/Either"
import { runGQLSubscription } from "@hoppscotch/common/helpers/backend/GQLClient"
import {
bulkApplySettings,
getDefaultSettings,
} from "@hoppscotch/common/newstore/settings"
import { runDispatchWithOutSyncing } from "@lib/sync"
function initSettingsSync() {
const currentUser$ = platformAuth.getCurrentUserStream()
settingsSyncer.startStoreSync()
settingsSyncer.setupSubscriptions(setupSubscriptions)
// load the settings
loadUserSettings()
currentUser$.subscribe(async (user) => {
if (user) {
// load the settings
loadUserSettings()
}
})
authEvents$.subscribe((event) => {
if (event.event == "login" || event.event == "token_refresh") {
settingsSyncer.startListeningToSubscriptions()
}
if (event.event == "logout") {
settingsSyncer.stopListeningToSubscriptions()
}
})
}
async function loadUserSettings() {
const res = await getUserSettings()
// create user settings if it doesn't exist
E.isLeft(res) &&
res.left.error == "user_settings/not_found" &&
(await createUserSettings(JSON.stringify(getDefaultSettings())))
if (E.isRight(res)) {
runDispatchWithOutSyncing(() => {
bulkApplySettings(JSON.parse(res.right.me.settings.properties))
})
}
}
function setupSubscriptions() {
let subs: ReturnType<typeof runGQLSubscription>[1][] = []
const userSettingsUpdatedSub = setupUserSettingsUpdatedSubscription()
subs = [userSettingsUpdatedSub]
return () => {
subs.forEach((sub) => sub.unsubscribe())
}
}
function setupUserSettingsUpdatedSubscription() {
const [userSettingsUpdated$, userSettingsUpdatedSub] =
runUserSettingsUpdatedSubscription()
userSettingsUpdated$.subscribe((res) => {
if (E.isRight(res)) {
runDispatchWithOutSyncing(() => {
bulkApplySettings(JSON.parse(res.right.userSettingsUpdated.properties))
})
}
})
return userSettingsUpdatedSub
}
export const def: SettingsPlatformDef = {
initSettingsSync,
}

View File

@@ -0,0 +1,21 @@
import { settingsStore } from "@hoppscotch/common/newstore/settings"
import { getSyncInitFunction } from "../../lib/sync"
import { StoreSyncDefinitionOf } from "../../lib/sync"
import { updateUserSettings } from "./settings.api"
export const settingsSyncDefinition: StoreSyncDefinitionOf<
typeof settingsStore
> = {
applySetting() {
updateUserSettings(JSON.stringify(settingsStore.value))
},
}
export const settingsSyncer = getSyncInitFunction(
settingsStore,
settingsSyncDefinition,
() => true
)

View File

@@ -0,0 +1,142 @@
import * as E from "fp-ts/Either"
import {
Interceptor,
InterceptorError,
RequestRunResult,
} from "../../../services/interceptor.service"
import axios, { AxiosRequestConfig, CancelToken } from "axios"
import { cloneDeep } from "lodash-es"
import { Body, HttpVerb, ResponseType, getClient } from '@tauri-apps/api/http'
export const preProcessRequest = (
req: AxiosRequestConfig
): AxiosRequestConfig => {
const reqClone = cloneDeep(req)
// If the parameters are URLSearchParams, inject them to URL instead
// This prevents issues of marshalling the URLSearchParams to the proxy
if (reqClone.params instanceof URLSearchParams) {
try {
const url = new URL(reqClone.url ?? "")
for (const [key, value] of reqClone.params.entries()) {
url.searchParams.append(key, value)
}
reqClone.url = url.toString()
} catch (e) {
// making this a non-empty block, so we can make the linter happy.
// we should probably use, allowEmptyCatch, or take the time to do something with the caught errors :)
}
reqClone.params = {}
}
return reqClone
}
async function runRequest(
req: AxiosRequestConfig,
cancelled: () => boolean
): RequestRunResult["response"] {
const timeStart = Date.now()
const processedReq = preProcessRequest(req)
try {
const client = await getClient()
if (cancelled()) {
client.drop()
return E.left("cancellation")
}
let body = Body.text(processedReq.data ?? "")
if (processedReq.data instanceof FormData) {
let body_data = {}
for (const entry of processedReq.data.entries()) {
const [name, value] = entry;
if (value instanceof File) {
let file_data = await value.arrayBuffer()
body_data[name] = {
file: new Uint8Array(file_data as number[]),
fileName: value.name
}
}
}
body = Body.form(body_data);
}
const res = await client.request({
method: processedReq.method as HttpVerb,
url: processedReq.url ?? "",
responseType: ResponseType.Binary,
headers: processedReq.headers,
body: body
});
if (cancelled()) {
client.drop()
return E.left("cancellation")
}
res.data = new Uint8Array(res.data as number[]).buffer;
const timeEnd = Date.now()
return E.right({
...res,
config: {
timeData: {
startTime: timeStart,
endTime: timeEnd,
},
},
})
} catch (e) {
const timeEnd = Date.now()
if (axios.isAxiosError(e) && e.response) {
return E.right({
...e.response,
config: {
timeData: {
startTime: timeStart,
endTime: timeEnd,
},
},
})
} else if (axios.isCancel(e)) {
return E.left("cancellation")
} else {
return E.left(<InterceptorError>{
humanMessage: {
heading: (t) => t("error.network_fail"),
description: (t) => t("helpers.network_fail"),
},
error: e,
})
}
}
}
export const localclientInterceptor: Interceptor = {
interceptorID: "localclient",
name: () => "localclient",
selectable: { type: "selectable" },
runRequest(req) {
const processedReq = preProcessRequest(req)
let cancelled = false
const checkCancelled = () => { return cancelled }
return {
cancel: () => { cancelled = true },
response: runRequest(processedReq, checkCancelled),
}
},
}

View File

@@ -0,0 +1,36 @@
import {
runMutation,
runGQLQuery,
} from "@hoppscotch/common/helpers/backend/GQLClient"
import {
GetCurrentRestSessionDocument,
GetCurrentRestSessionQuery,
GetCurrentRestSessionQueryVariables,
SessionType,
UpdateUserSessionDocument,
UpdateUserSessionMutation,
UpdateUserSessionMutationVariables,
} from "../../api/generated/graphql"
export const updateUserSession = (
currentSession: string,
sessionType: SessionType
) =>
runMutation<
UpdateUserSessionMutation,
UpdateUserSessionMutationVariables,
""
>(UpdateUserSessionDocument, {
sessionType,
currentSession,
})()
export const getCurrentRestSession = () =>
runGQLQuery<
GetCurrentRestSessionQuery,
GetCurrentRestSessionQueryVariables,
""
>({
query: GetCurrentRestSessionDocument,
variables: {},
})

View File

@@ -0,0 +1,37 @@
import { PersistableRESTTabState } from "@hoppscotch/common/helpers/rest/tab"
import { HoppUser } from "@hoppscotch/common/platform/auth"
import { TabStatePlatformDef } from "@hoppscotch/common/platform/tab"
import { def as platformAuth } from "@platform/auth"
import { getCurrentRestSession, updateUserSession } from "./tabState.api"
import { SessionType } from "../../api/generated/graphql"
import * as E from "fp-ts/Either"
async function writeCurrentTabState(
_: HoppUser,
persistableTabState: PersistableRESTTabState
) {
await updateUserSession(JSON.stringify(persistableTabState), SessionType.Rest)
}
async function loadTabStateFromSync(): Promise<PersistableRESTTabState | null> {
const currentUser = platformAuth.getCurrentUser()
if (!currentUser)
throw new Error("Cannot load request from sync without login")
const res = await getCurrentRestSession()
if (E.isRight(res)) {
const currentRESTSession = res.right.me.currentRESTSession
return currentRESTSession ? JSON.parse(currentRESTSession) : null
} else {
}
return null
}
export const def: TabStatePlatformDef = {
loadTabStateFromSync,
writeCurrentTabState,
}

View File

@@ -0,0 +1,7 @@
/// <reference types="vite/client" />
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
}