Browse Source

refactor: port REST environments related functionality to the new architecture

jamesgeorge007 9 months ago
parent
commit
553d9959f0

+ 1 - 1
packages/hoppscotch-common/locales/en.json

@@ -282,7 +282,7 @@
     "added": "Environment addition",
     "create_new": "Create new environment",
     "created": "Environment created",
-    "deleted": "Environment deletion",
+    "deleted": "Environment deleted",
     "duplicated": "Environment duplicated",
     "edit": "Edit Environment",
     "empty_variables": "No variables",

+ 161 - 6
packages/hoppscotch-common/src/components/environments/index.vue

@@ -11,7 +11,13 @@
         @edit-environment="editEnvironment('Global')"
       />
     </div>
-    <EnvironmentsMy v-show="environmentType.type === 'my-environments'" />
+    <EnvironmentsMy
+      v-show="environmentType.type === 'my-environments'"
+      @create-environment="createEnvironment"
+      @duplicate-environment="duplicateEnvironment"
+      @update-environment="updateEnvironment"
+      @delete-environment="removeEnvironment"
+    />
     <EnvironmentsTeams
       v-show="environmentType.type === 'team-environments'"
       :team="environmentType.selectedTeam"
@@ -41,7 +47,7 @@
     :show="showConfirmRemoveEnvModal"
     :title="`${t('confirm.remove_environment')}`"
     @hide-modal="showConfirmRemoveEnvModal = false"
-    @resolve="removeSelectedEnvironment()"
+    @resolve="removeSelectedEnvironment"
   />
 </template>
 
@@ -49,10 +55,12 @@
 import { useReadonlyStream, useStream } from "@composables/stream"
 import { Environment } from "@hoppscotch/data"
 import { useService } from "dioc/vue"
+import * as E from "fp-ts/Either"
 import * as TE from "fp-ts/TaskEither"
 import { pipe } from "fp-ts/function"
 import { isEqual } from "lodash-es"
 import { computed, ref, watch } from "vue"
+
 import { useI18n } from "~/composables/i18n"
 import { useToast } from "~/composables/toast"
 import { defineActionHandler } from "~/helpers/actions"
@@ -61,6 +69,7 @@ import { deleteTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironme
 import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter"
 import {
   deleteEnvironment,
+  environments$,
   getSelectedEnvironmentIndex,
   globalEnv$,
   selectedEnvironmentIndex$,
@@ -68,6 +77,7 @@ import {
 } from "~/newstore/environments"
 import { useLocalState } from "~/newstore/localstate"
 import { platform } from "~/platform"
+import { NewWorkspaceService } from "~/services/new-workspace"
 import { TeamWorkspace, WorkspaceService } from "~/services/workspace.service"
 
 const t = useI18n()
@@ -86,6 +96,7 @@ const environmentType = ref<EnvironmentsChooseType>({
 })
 
 const globalEnv = useReadonlyStream(globalEnv$, [])
+const environments = useReadonlyStream(environments$, [])
 
 const globalEnvironment = computed(() => ({
   v: 1 as const,
@@ -100,6 +111,7 @@ const currentUser = useReadonlyStream(
 )
 
 const workspaceService = useService(WorkspaceService)
+const newWorkspaceService = useService(NewWorkspaceService)
 const REMEMBERED_TEAM_ID = useLocalState("REMEMBERED_TEAM_ID")
 
 const adapter = new TeamEnvironmentAdapter(undefined)
@@ -107,6 +119,8 @@ const adapterLoading = useReadonlyStream(adapter.loading$, false)
 const adapterError = useReadonlyStream(adapter.error$, null)
 const teamEnvironmentList = useReadonlyStream(adapter.teamEnvironmentList$, [])
 
+const activeWorkspaceHandle = newWorkspaceService.activeWorkspaceHandle
+
 const loading = computed(
   () => adapterLoading.value && teamEnvironmentList.value.length === 0
 )
@@ -131,6 +145,7 @@ const updateEnvironmentType = (newEnvironmentType: EnvironmentType) => {
 
 const workspace = workspaceService.currentWorkspace
 
+// TODO: Replace with `newWorkspaceService`
 // Switch to my environments if workspace is personal and to team environments if workspace is team
 // also resets selected environment if workspace is personal and the previous selected environment was a team environment
 watch(workspace, (newWorkspace) => {
@@ -184,15 +199,23 @@ const editEnvironment = (environmentIndex: "Global") => {
   displayModalEdit(true)
 }
 
-const removeSelectedEnvironment = () => {
+const removeSelectedEnvironment = async () => {
   const selectedEnvIndex = getSelectedEnvironmentIndex()
-  if (selectedEnvIndex?.type === "NO_ENV_SELECTED") return
 
+  if (
+    selectedEnvIndex?.type === "NO_ENV_SELECTED" ||
+    !activeWorkspaceHandle.value
+  ) {
+    return
+  }
+
+  // TODO: Remove the check once the team workspace implementation is in place
   if (selectedEnvIndex?.type === "MY_ENV") {
-    deleteEnvironment(selectedEnvIndex.index)
-    toast.success(`${t("state.deleted")}`)
+    await removeEnvironment(selectedEnvIndex.index)
+    return
   }
 
+  // TODO: Remove once the team workspace implementation is in place
   if (selectedEnvIndex?.type === "TEAM_ENV") {
     pipe(
       deleteTeamEnvironment(selectedEnvIndex.teamEnvID),
@@ -208,6 +231,138 @@ const removeSelectedEnvironment = () => {
   }
 }
 
+const createEnvironment = async (newEnvironment: Environment) => {
+  if (!activeWorkspaceHandle.value) {
+    return
+  }
+
+  const environmentHandleResult =
+    await newWorkspaceService.createRESTEnvironment(
+      activeWorkspaceHandle.value,
+      newEnvironment
+    )
+
+  if (E.isLeft(environmentHandleResult)) {
+    // INVALID_WORKSPACE_HANDLE
+    return
+  }
+
+  // Workspace invalidated
+  if (environmentHandleResult.right.get().value.type === "invalid") {
+    // INVALID_WORKSPACE_HANDLE | ENVIRONMENT_DOES_NOT_EXIST
+    return
+  }
+
+  setSelectedEnvironmentIndex({
+    type: "MY_ENV",
+    index: environments.value.length - 1,
+  })
+
+  toast.success(t("environment.created"))
+}
+
+const duplicateEnvironment = async (environmentID: number) => {
+  if (!activeWorkspaceHandle.value) {
+    return
+  }
+
+  const environmentHandleResult =
+    await newWorkspaceService.getRESTEnvironmentHandle(
+      activeWorkspaceHandle.value,
+      environmentID
+    )
+
+  if (E.isLeft(environmentHandleResult)) {
+    // INVALID_WORKSPACE_HANDLE
+    return
+  }
+
+  const environmentHandle = environmentHandleResult.right
+
+  const duplicatedEnvironmentHandleResult =
+    await newWorkspaceService.duplicateRESTEnvironment(environmentHandle)
+
+  if (E.isLeft(duplicatedEnvironmentHandleResult)) {
+    // INVALID_WORKSPACE_HANDLE
+    return
+  }
+
+  const duplicatedEnvironmentHandleRef =
+    duplicatedEnvironmentHandleResult.right.get()
+
+  // Workspace invalidated
+  if (duplicatedEnvironmentHandleRef.value.type === "invalid") {
+    // INVALID_WORKSPACE_HANDLE | ENVIRONMENT_DOES_NOT_EXIST
+    return
+  }
+
+  toast.success(t("environment.duplicated"))
+}
+
+const updateEnvironment = async (
+  environmentID: number,
+  updatedEnvironment: Partial<Environment>
+) => {
+  if (!activeWorkspaceHandle.value) {
+    return
+  }
+
+  const environmentHandleResult =
+    await newWorkspaceService.getRESTEnvironmentHandle(
+      activeWorkspaceHandle.value,
+      environmentID
+    )
+
+  if (E.isLeft(environmentHandleResult)) {
+    // INVALID_WORKSPACE_HANDLE
+    return
+  }
+
+  const environmentHandle = environmentHandleResult.right
+
+  const updatedEnvironmentHandleResult =
+    await newWorkspaceService.updateRESTEnvironment(
+      environmentHandle,
+      updatedEnvironment
+    )
+
+  if (E.isLeft(updatedEnvironmentHandleResult)) {
+    // INVALID_WORKSPACE_HANDLE
+    return
+  }
+
+  toast.success(t("environment.updated"))
+}
+
+const removeEnvironment = async (environmentID: number) => {
+  if (!activeWorkspaceHandle.value) {
+    return
+  }
+
+  const environmentHandleResult =
+    await newWorkspaceService.getRESTEnvironmentHandle(
+      activeWorkspaceHandle.value,
+      environmentID
+    )
+
+  if (E.isLeft(environmentHandleResult)) {
+    // INVALID_WORKSPACE_HANDLE
+    return
+  }
+
+  const environmentHandle = environmentHandleResult.right
+
+  const updatedEnvironmentHandleResult =
+    await newWorkspaceService.removeRESTEnvironment(environmentHandle)
+
+  if (E.isLeft(updatedEnvironmentHandleResult)) {
+    // INVALID_WORKSPACE_HANDLE
+    return
+  }
+
+  toast.success(t("environment.deleted"))
+}
+
 const resetSelectedData = () => {
   editingEnvironmentIndex.value = null
   editingVariableName.value = ""

+ 29 - 49
packages/hoppscotch-common/src/components/environments/my/Details.vue

@@ -133,37 +133,34 @@
 </template>
 
 <script setup lang="ts">
-import IconTrash2 from "~icons/lucide/trash-2"
-import IconDone from "~icons/lucide/check"
-import IconPlus from "~icons/lucide/plus"
-import IconTrash from "~icons/lucide/trash"
-import IconHelpCircle from "~icons/lucide/help-circle"
-import { ComputedRef, computed, ref, watch } from "vue"
-import * as E from "fp-ts/Either"
-import * as A from "fp-ts/Array"
-import * as O from "fp-ts/Option"
-import { pipe, flow } from "fp-ts/function"
+import { useI18n } from "@composables/i18n"
+import { useReadonlyStream } from "@composables/stream"
+import { useColorMode } from "@composables/theming"
+import { useToast } from "@composables/toast"
 import { Environment, parseTemplateStringE } from "@hoppscotch/data"
 import { refAutoReset } from "@vueuse/core"
+import { useService } from "dioc/vue"
+import * as A from "fp-ts/Array"
+import * as E from "fp-ts/Either"
+import * as O from "fp-ts/Option"
+import { flow, pipe } from "fp-ts/function"
+import { ComputedRef, computed, ref, watch } from "vue"
+
+import { uniqueID } from "~/helpers/utils/uniqueID"
 import {
-  createEnvironment,
   environments$,
+  environmentsStore,
   getEnvironment,
   getGlobalVariables,
   globalEnv$,
   setGlobalEnvVariables,
-  setSelectedEnvironmentIndex,
-  updateEnvironment,
 } from "~/newstore/environments"
-import { useI18n } from "@composables/i18n"
-import { useToast } from "@composables/toast"
-import { useReadonlyStream } from "@composables/stream"
-import { useColorMode } from "@composables/theming"
-import { environmentsStore } from "~/newstore/environments"
-import { platform } from "~/platform"
-import { useService } from "dioc/vue"
 import { SecretEnvironmentService } from "~/services/secret-environment.service"
-import { uniqueID } from "~/helpers/utils/uniqueID"
+import IconDone from "~icons/lucide/check"
+import IconHelpCircle from "~icons/lucide/help-circle"
+import IconPlus from "~icons/lucide/plus"
+import IconTrash from "~icons/lucide/trash"
+import IconTrash2 from "~icons/lucide/trash-2"
 
 type EnvironmentVariable = {
   id: number
@@ -199,6 +196,12 @@ const props = withDefaults(
 
 const emit = defineEmits<{
   (e: "hide-modal"): void
+  (e: "create-environment", newEnvironment: Environment): void
+  (
+    e: "update-environment",
+    environmentID: number,
+    updatedEnvironment: Partial<Environment>
+  ): void
 }>()
 
 const idTicker = ref(0)
@@ -425,22 +428,7 @@ const saveEnvironment = () => {
   }
 
   if (props.action === "new") {
-    // Creating a new environment
-    createEnvironment(
-      editingName.value,
-      environmentUpdated.variables,
-      editingID.value
-    )
-    setSelectedEnvironmentIndex({
-      type: "MY_ENV",
-      index: envList.value.length - 1,
-    })
-    toast.success(`${t("environment.created")}`)
-
-    platform.analytics?.logEvent({
-      type: "HOPP_CREATE_ENVIRONMENT",
-      workspaceType: "personal",
-    })
+    emit("create-environment", { ...environmentUpdated, id: editingID.value })
   } else if (props.editingEnvironmentIndex === "Global") {
     // Editing the Global environment
     setGlobalEnvVariables(environmentUpdated.variables)
@@ -450,18 +438,10 @@ const saveEnvironment = () => {
       environmentsStore.value.environments[props.editingEnvironmentIndex].id
 
     // Editing an environment
-    updateEnvironment(
-      props.editingEnvironmentIndex,
-      envID
-        ? {
-            ...environmentUpdated,
-            id: envID,
-          }
-        : {
-            ...environmentUpdated,
-          }
-    )
-    toast.success(`${t("environment.updated")}`)
+    emit("update-environment", props.editingEnvironmentIndex, {
+      ...environmentUpdated,
+      ...(envID && { id: envID }),
+    })
   }
 
   hideModal()

+ 27 - 21
packages/hoppscotch-common/src/components/environments/my/Environment.vue

@@ -117,26 +117,22 @@
 </template>
 
 <script setup lang="ts">
-import IconMoreVertical from "~icons/lucide/more-vertical"
-import IconEdit from "~icons/lucide/edit"
-import IconCopy from "~icons/lucide/copy"
-import IconTrash2 from "~icons/lucide/trash-2"
-import { ref } from "vue"
-import { Environment } from "@hoppscotch/data"
-import { cloneDeep } from "lodash-es"
-import {
-  deleteEnvironment,
-  duplicateEnvironment,
-  createEnvironment,
-  getGlobalVariables,
-} from "~/newstore/environments"
 import { useI18n } from "@composables/i18n"
 import { useToast } from "@composables/toast"
-import { TippyComponent } from "vue-tippy"
+import { Environment } from "@hoppscotch/data"
 import { HoppSmartItem } from "@hoppscotch/ui"
-import { exportAsJSON } from "~/helpers/import-export/export/environment"
 import { useService } from "dioc/vue"
+import { cloneDeep } from "lodash-es"
+import { ref } from "vue"
+import { TippyComponent } from "vue-tippy"
+
+import { exportAsJSON } from "~/helpers/import-export/export/environment"
+import { createEnvironment, getGlobalVariables } from "~/newstore/environments"
 import { SecretEnvironmentService } from "~/services/secret-environment.service"
+import IconCopy from "~icons/lucide/copy"
+import IconEdit from "~icons/lucide/edit"
+import IconMoreVertical from "~icons/lucide/more-vertical"
+import IconTrash2 from "~icons/lucide/trash-2"
 
 const t = useI18n()
 const toast = useToast()
@@ -148,6 +144,8 @@ const props = defineProps<{
 
 const emit = defineEmits<{
   (e: "edit-environment"): void
+  (e: "duplicate-environment", environmentID: number): void
+  (e: "delete-environment", environmentID: number): void
 }>()
 
 const confirmRemove = ref(false)
@@ -169,23 +167,31 @@ const exportAsJsonEl = ref<typeof HoppSmartItem>()
 const deleteAction = ref<typeof HoppSmartItem>()
 
 const removeEnvironment = () => {
-  if (props.environmentIndex === null) return
+  if (props.environmentIndex === null) {
+    return
+  }
+
   if (props.environmentIndex !== "Global") {
-    deleteEnvironment(props.environmentIndex, props.environment.id)
+    emit("delete-environment", props.environmentIndex)
+
     secretEnvironmentService.deleteSecretEnvironment(props.environment.id)
   }
-  toast.success(`${t("state.deleted")}`)
 }
 
 const duplicateEnvironments = () => {
-  if (props.environmentIndex === null) return
+  if (props.environmentIndex === null) {
+    return
+  }
+
   if (props.environmentIndex === "Global") {
     createEnvironment(
       `Global - ${t("action.duplicate")}`,
       cloneDeep(getGlobalVariables())
     )
-  } else duplicateEnvironment(props.environmentIndex)
 
-  toast.success(`${t("environment.duplicated")}`)
+    return
+  }
+
+  emit("duplicate-environment", props.environmentIndex)
 }
 </script>

+ 25 - 6
packages/hoppscotch-common/src/components/environments/my/index.vue

@@ -31,6 +31,8 @@
       :environment-index="index"
       :environment="environment"
       @edit-environment="editEnvironment(index)"
+      @duplicate-environment="emit('duplicate-environment', $event)"
+      @delete-environment="emit('delete-environment', $event)"
     />
     <HoppSmartPlaceholder
       v-if="!environments.length"
@@ -68,6 +70,11 @@
       :editing-environment-index="editingEnvironmentIndex"
       :editing-variable-name="editingVariableName"
       :is-secret-option-selected="secretOptionSelected"
+      @create-environment="emit('create-environment', $event)"
+      @update-environment="
+        (environmentID, updatedEnvironment) =>
+          emit('update-environment', environmentID, updatedEnvironment)
+      "
       @hide-modal="displayModalEdit(false)"
     />
     <EnvironmentsImportExport
@@ -79,22 +86,34 @@
 </template>
 
 <script setup lang="ts">
-import { ref } from "vue"
-import { environments$ } from "~/newstore/environments"
-import { useColorMode } from "~/composables/theming"
 import { useReadonlyStream } from "@composables/stream"
+import { Environment } from "@hoppscotch/data"
+import { ref } from "vue"
 import { useI18n } from "~/composables/i18n"
-import IconPlus from "~icons/lucide/plus"
+
+import { useColorMode } from "~/composables/theming"
+import { defineActionHandler } from "~/helpers/actions"
+import { environments$ } from "~/newstore/environments"
 import IconImport from "~icons/lucide/folder-down"
 import IconHelpCircle from "~icons/lucide/help-circle"
-import { Environment } from "@hoppscotch/data"
-import { defineActionHandler } from "~/helpers/actions"
+import IconPlus from "~icons/lucide/plus"
 
 const t = useI18n()
 const colorMode = useColorMode()
 
 const environments = useReadonlyStream(environments$, [])
 
+const emit = defineEmits<{
+  (e: "create-environment", newEnvironment: Environment): void
+  (e: "duplicate-environment", environmentID: number): void
+  (
+    e: "update-environment",
+    environmentID: number,
+    updatedEnvironment: Partial<Environment>
+  ): void
+  (e: "delete-environment", environmentID: number): void
+}>()
+
 const showModalImportExport = ref(false)
 const showModalDetails = ref(false)
 const action = ref<"new" | "edit">("edit")

+ 0 - 5
packages/hoppscotch-common/src/components/new-collections/rest/index.vue

@@ -1630,11 +1630,6 @@ const dropToRoot = async ({ dataTransfer }: DragEvent) => {
     restCollectionState.value.length - 1
   ).toString()
 
-  // updateSaveContextForAffectedRequests(
-  //   draggedCollectionIndex,
-  //   destinationRootCollectionIndex
-  // )
-
   const destinationRootCollectionHandleResult =
     await workspaceService.getCollectionHandle(
       props.workspaceHandle,

+ 21 - 1
packages/hoppscotch-common/src/services/new-workspace/helpers.ts

@@ -1,6 +1,11 @@
 import { Ref } from "vue"
 import { HandleRef } from "./handle"
-import { Workspace, WorkspaceCollection, WorkspaceRequest } from "./workspace"
+import {
+  Workspace,
+  WorkspaceCollection,
+  WorkspaceEnvironment,
+  WorkspaceRequest,
+} from "./workspace"
 
 export const isValidWorkspaceHandle = (
   workspaceHandle: HandleRef<Workspace>,
@@ -46,3 +51,18 @@ export const isValidRequestHandle = (
     requestHandle.value.data.workspaceID === workspaceID
   )
 }
+
+export const isValidEnvironmentHandle = (
+  environmentHandle: HandleRef<WorkspaceEnvironment>,
+  providerID: string,
+  workspaceID: string
+): environmentHandle is Ref<{
+  data: WorkspaceEnvironment
+  type: "ok"
+}> => {
+  return (
+    environmentHandle.value.type === "ok" &&
+    environmentHandle.value.data.providerID === providerID &&
+    environmentHandle.value.data.workspaceID === workspaceID
+  )
+}

+ 168 - 1
packages/hoppscotch-common/src/services/new-workspace/index.ts

@@ -1,4 +1,5 @@
 import {
+  Environment,
   HoppCollection,
   HoppGQLRequest,
   HoppRESTRequest,
@@ -23,7 +24,12 @@ import {
   SearchResultsView,
   RootRESTCollectionView,
 } from "./view"
-import { Workspace, WorkspaceCollection, WorkspaceRequest } from "./workspace"
+import {
+  Workspace,
+  WorkspaceCollection,
+  WorkspaceEnvironment,
+  WorkspaceRequest,
+} from "./workspace"
 
 export type WorkspaceError<ServiceErr> =
   | { type: "SERVICE_ERROR"; error: ServiceErr }
@@ -189,6 +195,41 @@ export class NewWorkspaceService extends Service {
     return E.right(result.right)
   }
 
+  public async getRESTEnvironmentHandle(
+    workspaceHandle: Handle<Workspace>,
+    environmentID: number
+  ): Promise<
+    E.Either<
+      WorkspaceError<"INVALID_HANDLE" | "INVALID_PROVIDER">,
+      Handle<WorkspaceEnvironment>
+    >
+  > {
+    const workspaceHandleRef = workspaceHandle.get()
+
+    if (workspaceHandleRef.value.type === "invalid") {
+      return E.left({ type: "SERVICE_ERROR", error: "INVALID_HANDLE" })
+    }
+
+    const provider = this.registeredProviders.get(
+      workspaceHandleRef.value.data.providerID
+    )
+
+    if (!provider) {
+      return E.left({ type: "SERVICE_ERROR", error: "INVALID_PROVIDER" })
+    }
+
+    const result = await provider.getRESTEnvironmentHandle(
+      workspaceHandle,
+      environmentID
+    )
+
+    if (E.isLeft(result)) {
+      return E.left({ type: "PROVIDER_ERROR", error: result.left })
+    }
+
+    return E.right(result.right)
+  }
+
   public async createRESTRootCollection(
     workspaceHandle: Handle<Workspace>,
     newCollection: Partial<Exclude<HoppCollection, "id">> & { name: string }
@@ -1113,6 +1154,132 @@ export class NewWorkspaceService extends Service {
     return E.right(result.right)
   }
 
+  public async createRESTEnvironment(
+    workspaceHandle: Handle<Workspace>,
+    newEnvironment: Partial<Environment> & { name: string }
+  ): Promise<
+    E.Either<
+      WorkspaceError<"INVALID_HANDLE" | "INVALID_PROVIDER">,
+      Handle<WorkspaceEnvironment>
+    >
+  > {
+    const workspaceHandleRef = workspaceHandle.get()
+
+    if (workspaceHandleRef.value.type === "invalid") {
+      return E.left({ type: "SERVICE_ERROR", error: "INVALID_HANDLE" })
+    }
+
+    const provider = this.registeredProviders.get(
+      workspaceHandleRef.value.data.providerID
+    )
+
+    if (!provider) {
+      return E.left({ type: "SERVICE_ERROR", error: "INVALID_PROVIDER" })
+    }
+
+    const result = await provider.createRESTEnvironment(
+      workspaceHandle,
+      newEnvironment
+    )
+
+    if (E.isLeft(result)) {
+      return E.left({ type: "PROVIDER_ERROR", error: result.left })
+    }
+
+    return E.right(result.right)
+  }
+
+  public async duplicateRESTEnvironment(
+    environmentHandle: Handle<WorkspaceEnvironment>
+  ): Promise<
+    E.Either<
+      WorkspaceError<"INVALID_HANDLE" | "INVALID_PROVIDER">,
+      Handle<WorkspaceEnvironment>
+    >
+  > {
+    const environmentHandleRef = environmentHandle.get()
+
+    if (environmentHandleRef.value.type === "invalid") {
+      return E.left({ type: "SERVICE_ERROR", error: "INVALID_HANDLE" })
+    }
+
+    const provider = this.registeredProviders.get(
+      environmentHandleRef.value.data.providerID
+    )
+
+    if (!provider) {
+      return E.left({ type: "SERVICE_ERROR", error: "INVALID_PROVIDER" })
+    }
+
+    const result = await provider.duplicateRESTEnvironment(environmentHandle)
+
+    if (E.isLeft(result)) {
+      return E.left({ type: "PROVIDER_ERROR", error: result.left })
+    }
+
+    return E.right(result.right)
+  }
+
+  public async updateRESTEnvironment(
+    environmentHandle: Handle<WorkspaceEnvironment>,
+    updatedEnvironment: Partial<Environment>
+  ): Promise<
+    E.Either<WorkspaceError<"INVALID_HANDLE" | "INVALID_PROVIDER">, void>
+  > {
+    const environmentHandleRef = environmentHandle.get()
+
+    if (environmentHandleRef.value.type === "invalid") {
+      return E.left({ type: "SERVICE_ERROR", error: "INVALID_HANDLE" })
+    }
+
+    const provider = this.registeredProviders.get(
+      environmentHandleRef.value.data.providerID
+    )
+
+    if (!provider) {
+      return E.left({ type: "SERVICE_ERROR", error: "INVALID_PROVIDER" })
+    }
+
+    const result = await provider.updateRESTEnvironment(
+      environmentHandle,
+      updatedEnvironment
+    )
+
+    if (E.isLeft(result)) {
+      return E.left({ type: "PROVIDER_ERROR", error: result.left })
+    }
+
+    return E.right(result.right)
+  }
+
+  public async removeRESTEnvironment(
+    environmentHandle: Handle<WorkspaceEnvironment>
+  ): Promise<
+    E.Either<WorkspaceError<"INVALID_HANDLE" | "INVALID_PROVIDER">, void>
+  > {
+    const environmentHandleRef = environmentHandle.get()
+
+    if (environmentHandleRef.value.type === "invalid") {
+      return E.left({ type: "SERVICE_ERROR", error: "INVALID_HANDLE" })
+    }
+
+    const provider = this.registeredProviders.get(
+      environmentHandleRef.value.data.providerID
+    )
+
+    if (!provider) {
+      return E.left({ type: "SERVICE_ERROR", error: "INVALID_PROVIDER" })
+    }
+
+    const result = await provider.removeRESTEnvironment(environmentHandle)
+
+    if (E.isLeft(result)) {
+      return E.left({ type: "PROVIDER_ERROR", error: result.left })
+    }
+
+    return E.right(result.right)
+  }
+
   public registerWorkspaceProvider(provider: WorkspaceProvider) {
     if (this.registeredProviders.has(provider.providerID)) {
       console.warn(

+ 23 - 2
packages/hoppscotch-common/src/services/new-workspace/provider.ts

@@ -2,22 +2,24 @@ import * as E from "fp-ts/Either"
 import { Ref } from "vue"
 
 import {
+  Environment,
   HoppCollection,
   HoppGQLRequest,
   HoppRESTRequest,
 } from "@hoppscotch/data"
 import { Handle } from "./handle"
 import {
-  RESTCollectionChildrenView,
   CollectionJSONView,
   CollectionLevelAuthHeadersView,
-  SearchResultsView,
+  RESTCollectionChildrenView,
   RootRESTCollectionView,
+  SearchResultsView,
 } from "./view"
 import {
   Workspace,
   WorkspaceCollection,
   WorkspaceDecor,
+  WorkspaceEnvironment,
   WorkspaceRequest,
 } from "./workspace"
 
@@ -41,6 +43,10 @@ export interface WorkspaceProvider {
     requestID: string,
     type: "REST" | "GQL"
   ): Promise<E.Either<unknown, Handle<WorkspaceRequest>>>
+  getRESTEnvironmentHandle(
+    workspaceHandle: Handle<Workspace>,
+    environmentID: number
+  ): Promise<E.Either<unknown, Handle<WorkspaceEnvironment>>>
 
   getCollectionLevelAuthHeadersView(
     collectionHandle: Handle<WorkspaceCollection>,
@@ -156,4 +162,19 @@ export interface WorkspaceProvider {
   exportGQLCollections(
     workspaceHandle: Handle<Workspace>
   ): Promise<E.Either<unknown, void>>
+
+  createRESTEnvironment(
+    workspaceHandle: Handle<Workspace>,
+    newEnvironment: Partial<Environment> & { name: string }
+  ): Promise<E.Either<unknown, Handle<WorkspaceEnvironment>>>
+  duplicateRESTEnvironment(
+    environmentHandle: Handle<WorkspaceEnvironment>
+  ): Promise<E.Either<unknown, Handle<WorkspaceEnvironment>>>
+  updateRESTEnvironment(
+    environmentHandle: Handle<WorkspaceEnvironment>,
+    updatedEnvironment: Partial<Environment>
+  ): Promise<E.Either<unknown, void>>
+  removeRESTEnvironment(
+    environmentHandle: Handle<WorkspaceEnvironment>
+  ): Promise<E.Either<unknown, void>>
 }

+ 223 - 2
packages/hoppscotch-common/src/services/new-workspace/providers/personal.workspace.ts

@@ -1,4 +1,5 @@
 import {
+  Environment,
   HoppCollection,
   HoppGQLRequest,
   HoppRESTRequest,
@@ -71,6 +72,7 @@ import {
   Workspace,
   WorkspaceCollection,
   WorkspaceDecor,
+  WorkspaceEnvironment,
   WorkspaceRequest,
 } from "~/services/new-workspace/workspace"
 
@@ -80,10 +82,18 @@ import { getRequestsByPath } from "~/helpers/collection/request"
 import { initializeDownloadFile } from "~/helpers/import-export/export"
 import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
 import { lazy } from "~/helpers/utils/lazy"
+import {
+  createEnvironment,
+  deleteEnvironment,
+  duplicateEnvironment,
+  environments$,
+  updateEnvironment,
+} from "~/newstore/environments"
 import IconUser from "~icons/lucide/user"
 import { NewWorkspaceService } from ".."
 import {
   isValidCollectionHandle,
+  isValidEnvironmentHandle,
   isValidRequestHandle,
   isValidWorkspaceHandle,
 } from "../helpers"
@@ -112,6 +122,8 @@ export class PersonalWorkspaceProviderService
     state: [],
   })
 
+  public restEnvironmentState: Ref<Environment[]> = ref([])
+
   // Issued handles can have collection handles when the collection runner is introduced
   public issuedHandles: WritableHandleRef<
     WorkspaceRequest | WorkspaceCollection
@@ -134,6 +146,10 @@ export class PersonalWorkspaceProviderService
       }
     )[0]
 
+    this.restEnvironmentState = useStreamStatic(environments$, [], () => {
+      /* noop */
+    })[0]
+
     this.workspaceService.registerWorkspaceProvider(this)
   }
 
@@ -455,8 +471,9 @@ export class PersonalWorkspaceProviderService
 
     // TODO: Verify whether a request update action is reflected correctly in the handle being returned below
 
+    const personalWorkspaceHandle = this.getPersonalWorkspaceHandle()
     const createdRequestHandle = await this.getRequestHandle(
-      parentCollectionHandle,
+      personalWorkspaceHandle,
       requestID,
       "REST"
     )
@@ -1300,6 +1317,59 @@ export class PersonalWorkspaceProviderService
     return Promise.resolve(E.right({ get: lazy(() => handle) }))
   }
 
+  public getRESTEnvironmentHandle(
+    workspaceHandle: Handle<Workspace>,
+    environmentID: number
+  ): Promise<E.Either<unknown, Handle<WorkspaceEnvironment>>> {
+    const workspaceHandleRef = workspaceHandle.get()
+
+    if (
+      !isValidWorkspaceHandle(workspaceHandleRef, this.providerID, "personal")
+    ) {
+      return Promise.resolve(E.left("INVALID_WORKSPACE_HANDLE" as const))
+    }
+
+    const environment = this.restEnvironmentState.value[environmentID]
+
+    // Out of bounds check
+    if (!environment) {
+      return Promise.resolve(E.left("ENVIRONMENT_DOES_NOT_EXIST"))
+    }
+
+    return Promise.resolve(
+      E.right({
+        get: lazy(() =>
+          computed(() => {
+            if (
+              !isValidWorkspaceHandle(
+                workspaceHandleRef,
+                this.providerID,
+                "personal"
+              )
+            ) {
+              return {
+                type: "invalid" as const,
+                reason: "INVALID_WORKSPACE_HANDLE" as const,
+              }
+            }
+
+            const { providerID, workspaceID } = workspaceHandleRef.value.data
+
+            return {
+              type: "ok",
+              data: {
+                providerID,
+                workspaceID,
+                environmentID,
+                name: environment.name,
+              },
+            }
+          })
+        ),
+      })
+    )
+  }
+
   public getCollectionLevelAuthHeadersView(
     collectionHandle: Handle<WorkspaceCollection>,
     type: "REST" | "GQL"
@@ -1857,8 +1927,9 @@ export class PersonalWorkspaceProviderService
 
     // TODO: Verify whether a request update action is reflected correctly in the handle being returned below
 
+    const personalWorkspaceHandle = this.getPersonalWorkspaceHandle()
     const createdRequestHandle = await this.getRequestHandle(
-      parentCollectionHandle,
+      personalWorkspaceHandle,
       requestID,
       "GQL"
     )
@@ -2264,6 +2335,156 @@ export class PersonalWorkspaceProviderService
     return Promise.resolve(E.right(undefined))
   }
 
+  public async createRESTEnvironment(
+    workspaceHandle: Handle<Workspace>,
+    newEnvironment: Partial<Environment> & { name: string }
+  ): Promise<E.Either<unknown, Handle<WorkspaceEnvironment>>> {
+    const workspaceHandleRef = workspaceHandle.get()
+
+    if (
+      !isValidWorkspaceHandle(workspaceHandleRef, this.providerID, "personal")
+    ) {
+      return Promise.resolve(E.left("INVALID_WORKSPACE_HANDLE" as const))
+    }
+
+    createEnvironment(
+      newEnvironment.name,
+      newEnvironment.variables,
+      newEnvironment.id
+    )
+
+    platform.analytics?.logEvent({
+      type: "HOPP_CREATE_ENVIRONMENT",
+      workspaceType: "personal",
+    })
+
+    const createdEnvironmentHandle = await this.getRESTEnvironmentHandle(
+      workspaceHandle,
+      this.restEnvironmentState.value.length - 1
+    )
+
+    return createdEnvironmentHandle
+  }
+
+  public async duplicateRESTEnvironment(
+    environmentHandle: Handle<WorkspaceEnvironment>
+  ): Promise<E.Either<unknown, Handle<WorkspaceEnvironment>>> {
+    const environmentHandleRef = environmentHandle.get()
+
+    if (
+      !isValidEnvironmentHandle(
+        environmentHandleRef,
+        this.providerID,
+        "personal"
+      )
+    ) {
+      return Promise.resolve(E.left("INVALID_ENVIRONMENT_HANDLE" as const))
+    }
+
+    duplicateEnvironment(environmentHandleRef.value.data.environmentID)
+
+    const createdEnvironmentHandle = await this.getRESTEnvironmentHandle(
+      this.getPersonalWorkspaceHandle(),
+      this.restEnvironmentState.value.length - 1
+    )
+
+    return createdEnvironmentHandle
+  }
+
+  public async updateRESTEnvironment(
+    environmentHandle: Handle<WorkspaceEnvironment>,
+    updatedEnvironment: Partial<Environment>
+  ): Promise<E.Either<unknown, void>> {
+    const environmentHandleRef = environmentHandle.get()
+
+    if (
+      !isValidEnvironmentHandle(
+        environmentHandleRef,
+        this.providerID,
+        "personal"
+      )
+    ) {
+      return Promise.resolve(E.left("INVALID_ENVIRONMENT_HANDLE" as const))
+    }
+
+    const { environmentID } = environmentHandleRef.value.data
+
+    const existingEnvironment =
+      this.restEnvironmentState.value[
+        environmentHandleRef.value.data.environmentID
+      ]
+
+    const { id: environmentSyncID } = existingEnvironment
+
+    const newEnvironment = {
+      ...existingEnvironment,
+      ...updatedEnvironment,
+    }
+
+    updateEnvironment(
+      environmentID,
+      environmentSyncID
+        ? {
+            ...newEnvironment,
+            id: environmentSyncID,
+          }
+        : {
+            ...newEnvironment,
+          }
+    )
+
+    const updatedEnvironmentHandle = await this.getRESTEnvironmentHandle(
+      this.getPersonalWorkspaceHandle(),
+      environmentID
+    )
+
+    if (E.isRight(updatedEnvironmentHandle)) {
+      const updatedEnvironmentHandleRef = updatedEnvironmentHandle.right.get()
+
+      if (updatedEnvironmentHandleRef.value.type === "ok") {
+        updatedEnvironmentHandleRef.value.data.name = newEnvironment.name
+      }
+    }
+
+    return Promise.resolve(E.right(undefined))
+  }
+
+  public async removeRESTEnvironment(
+    environmentHandle: Handle<WorkspaceEnvironment>
+  ): Promise<E.Either<unknown, void>> {
+    const environmentHandleRef = environmentHandle.get()
+
+    if (
+      !isValidEnvironmentHandle(
+        environmentHandleRef,
+        this.providerID,
+        "personal"
+      )
+    ) {
+      return Promise.resolve(E.left("INVALID_ENVIRONMENT_HANDLE" as const))
+    }
+
+    const removedEnvironmentHandle = await this.getRESTEnvironmentHandle(
+      this.getPersonalWorkspaceHandle(),
+      environmentHandleRef.value.data.environmentID
+    )
+
+    if (E.isRight(removedEnvironmentHandle)) {
+      const removedEnvironmentHandleRef = removedEnvironmentHandle.right.get()
+
+      if (removedEnvironmentHandleRef.value.type === "ok") {
+        removedEnvironmentHandleRef.value = {
+          type: "invalid",
+          reason: "ENVIRONMENT_INVALIDATED",
+        }
+      }
+    }
+
+    deleteEnvironment(environmentHandleRef.value.data.environmentID)
+
+    return Promise.resolve(E.right(undefined))
+  }
+
   public getWorkspaceHandle(
     workspaceID: string
   ): Promise<E.Either<unknown, Handle<Workspace>>> {

Some files were not shown because too many files changed in this diff