feat: desktop app
Co-authored-by: Vivek R <123vivekr@gmail.com> Co-authored-by: Liyas Thomas <liyascthomas@gmail.com>
This commit is contained in:
374
packages/hoppscotch-selfhost-desktop/src/platform/auth.ts
Normal file
374
packages/hoppscotch-selfhost-desktop/src/platform/auth.ts
Normal file
@@ -0,0 +1,374 @@
|
||||
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";
|
||||
|
||||
export const authEvents$ = new Subject<AuthEvent | { event: "token_refresh" }>()
|
||||
const currentUser$ = new BehaviorSubject<HoppUser | null>(null)
|
||||
export const probableUser$ = new BehaviorSubject<HoppUser | null>(null)
|
||||
|
||||
const APP_DATA_PATH = "~/.hopp-desktop-app-data.dat"
|
||||
|
||||
async function logout() {
|
||||
let client = await getClient();
|
||||
await client.get(`${import.meta.env.VITE_BACKEND_API_URL}/auth/logout`)
|
||||
|
||||
const store = new Store(APP_DATA_PATH)
|
||||
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(APP_DATA_PATH);
|
||||
|
||||
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(APP_DATA_PATH);
|
||||
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(APP_DATA_PATH)
|
||||
|
||||
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(APP_DATA_PATH)
|
||||
|
||||
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",
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -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: {} })
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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")
|
||||
)
|
||||
@@ -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: {},
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
)
|
||||
@@ -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: {},
|
||||
})
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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")
|
||||
)
|
||||
@@ -0,0 +1,169 @@
|
||||
import * as E from "fp-ts/Either"
|
||||
import {
|
||||
Interceptor,
|
||||
InterceptorError,
|
||||
RequestRunResult,
|
||||
} from "@hoppscotch/common/services/interceptor.service"
|
||||
import { CookieJarService } from "@hoppscotch/common/services/cookie-jar.service"
|
||||
import axios, { AxiosRequestConfig, CancelToken } from "axios"
|
||||
import { cloneDeep } from "lodash-es"
|
||||
import { Body, HttpVerb, ResponseType, getClient } from "@tauri-apps/api/http"
|
||||
import { Service } from "dioc"
|
||||
|
||||
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),
|
||||
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 class NativeInterceptorService extends Service implements Interceptor {
|
||||
public static readonly ID = "NATIVE_INTERCEPTOR_SERVICE"
|
||||
|
||||
public interceptorID = "native" // TODO: i18n this
|
||||
|
||||
public name = () => "Native"
|
||||
|
||||
public selectable = { type: "selectable" as const }
|
||||
|
||||
public supportsCookies = true
|
||||
|
||||
public cookieJarService = this.bind(CookieJarService)
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
}
|
||||
|
||||
public runRequest(req: any) {
|
||||
const processedReq = preProcessRequest(req)
|
||||
|
||||
const relevantCookies = this.cookieJarService.getCookiesForURL(
|
||||
new URL(processedReq.url!)
|
||||
)
|
||||
|
||||
processedReq.headers["Cookie"] = relevantCookies
|
||||
.map((cookie) => `${cookie.name!}=${cookie.value!}`)
|
||||
.join(";")
|
||||
|
||||
let cancelled = false
|
||||
|
||||
const checkCancelled = () => {
|
||||
return cancelled
|
||||
}
|
||||
|
||||
return {
|
||||
cancel: () => {
|
||||
cancelled = true
|
||||
},
|
||||
response: runRequest(processedReq, checkCancelled),
|
||||
}
|
||||
}
|
||||
}
|
||||
24
packages/hoppscotch-selfhost-desktop/src/platform/io.ts
Normal file
24
packages/hoppscotch-selfhost-desktop/src/platform/io.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { IOPlatformDef } from "@hoppscotch/common/platform/io"
|
||||
import { save } from "@tauri-apps/api/dialog"
|
||||
import { writeBinaryFile, writeTextFile } from "@tauri-apps/api/fs"
|
||||
|
||||
export const ioDef: IOPlatformDef = {
|
||||
async saveFileWithDialog(opts) {
|
||||
const path = await save({
|
||||
filters: opts.filters,
|
||||
defaultPath: opts.suggestedFilename,
|
||||
})
|
||||
|
||||
if (path === null) {
|
||||
return { type: "cancelled" }
|
||||
}
|
||||
|
||||
if (typeof opts.data === "string") {
|
||||
await writeTextFile(path, opts.data)
|
||||
} else {
|
||||
await writeBinaryFile(path, opts.data)
|
||||
}
|
||||
|
||||
return { type: "saved", path }
|
||||
},
|
||||
}
|
||||
@@ -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: {},
|
||||
})
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
@@ -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: {},
|
||||
})
|
||||
@@ -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,
|
||||
}
|
||||
Reference in New Issue
Block a user