From 03212386fb4f2b2be96a2b7c4e3484386b1c4985 Mon Sep 17 00:00:00 2001 From: Anwarul Islam Date: Tue, 25 Nov 2025 22:33:21 +0600 Subject: [PATCH] feat: add platform-specific import support for personal collections (#5570) Co-authored-by: mirarifhasan Co-authored-by: James George <25279263+jamesgeorge007@users.noreply.github.com> --- .../user-collection.resolver.ts | 3 +- .../user-collection.service.ts | 29 ++- .../user-collection/user-collections.model.ts | 13 ++ .../hoppscotch-common/src/components.d.ts | 3 + .../components/collections/ImportExport.vue | 63 ++---- .../collections/graphql/ImportExport.vue | 62 ++---- .../ImportUserCollectionsFromJSON.graphql | 11 - .../queries/GetUserRootCollections.graphql | 34 --- .../backend/mutations/UserCollection.ts | 200 ------------------ .../src/platform/collections.ts | 8 + .../ImportUserCollectionsFromJSON.graphql | 5 +- .../src/platform/collections/desktop/api.ts | 36 +--- .../platform/collections/desktop/import.ts | 1 + .../src/platform/collections/desktop/index.ts | 9 +- .../src/platform/collections/web/api.ts | 36 +--- .../src/platform/collections/web/import.ts | 112 ++++++++++ .../src/platform/collections/web/index.ts | 8 +- 17 files changed, 214 insertions(+), 419 deletions(-) delete mode 100644 packages/hoppscotch-common/src/helpers/backend/gql/mutations/ImportUserCollectionsFromJSON.graphql delete mode 100644 packages/hoppscotch-common/src/helpers/backend/gql/queries/GetUserRootCollections.graphql delete mode 100644 packages/hoppscotch-common/src/helpers/backend/mutations/UserCollection.ts create mode 100644 packages/hoppscotch-selfhost-web/src/platform/collections/desktop/import.ts create mode 100644 packages/hoppscotch-selfhost-web/src/platform/collections/web/import.ts diff --git a/packages/hoppscotch-backend/src/user-collection/user-collection.resolver.ts b/packages/hoppscotch-backend/src/user-collection/user-collection.resolver.ts index 7302715e..c1da4a6d 100644 --- a/packages/hoppscotch-backend/src/user-collection/user-collection.resolver.ts +++ b/packages/hoppscotch-backend/src/user-collection/user-collection.resolver.ts @@ -18,6 +18,7 @@ import { UserCollection, UserCollectionDuplicatedData, UserCollectionExportJSONData, + UserCollectionImportResult, UserCollectionRemovedData, UserCollectionReorderData, } from './user-collections.model'; @@ -375,7 +376,7 @@ export class UserCollectionResolver { return res.right; } - @Mutation(() => Boolean, { + @Mutation(() => UserCollectionExportJSONData, { description: 'Import collections from JSON string to the specified Team', }) @UseGuards(GqlAuthGuard) diff --git a/packages/hoppscotch-backend/src/user-collection/user-collection.service.ts b/packages/hoppscotch-backend/src/user-collection/user-collection.service.ts index 076f439d..5d89c04b 100644 --- a/packages/hoppscotch-backend/src/user-collection/user-collection.service.ts +++ b/packages/hoppscotch-backend/src/user-collection/user-collection.service.ts @@ -40,6 +40,7 @@ import { import { CollectionFolder } from 'src/types/CollectionFolder'; import { PrismaError } from 'src/prisma/prisma-error-codes'; import { SortOptions } from 'src/types/SortOptions'; +import { UserRequest } from 'src/user-request/user-request.model'; @Injectable() export class UserCollectionService { @@ -970,7 +971,7 @@ export class UserCollectionService { collectionListObjects.push(result.right); } - // If collectionID is not null, return JSONified data for specific collection + // If collectionID is not null, return JSON stringified data for specific collection if (collectionID) { // Get Details of collection const parentCollection = await this.getUserCollection(collectionID); @@ -1033,7 +1034,10 @@ export class UserCollectionService { let data = null; if (folder.data) { try { - data = JSON.parse(folder.data); + data = + typeof folder.data === 'string' + ? JSON.parse(folder.data) + : folder.data; } catch (error) { // If data parsing fails, log error and continue without data console.error('Failed to parse collection data:', error); @@ -1149,14 +1153,24 @@ export class UserCollectionService { return E.left(USER_COLLECTION_CREATION_FAILED); } + // Fetch nested collections after transaction is committed + const importedCollectionsWithChildren: CollectionFolder[] = []; + for (const userCollection of userCollections) { + const exportedCollectionJSON = + await this.exportUserCollectionToJSONObject(userID, userCollection.id); + if (E.isLeft(exportedCollectionJSON)) + return E.left(exportedCollectionJSON.left); + importedCollectionsWithChildren.push(exportedCollectionJSON.right); + } + if (isCollectionDuplication) { - const collectionData = await this.fetchCollectionData( + const duplicatedCollectionData = await this.fetchCollectionData( userCollections[0].id, ); - if (E.isRight(collectionData)) { + if (E.isRight(duplicatedCollectionData)) { this.pubsub.publish( `user_coll/${userID}/duplicated`, - collectionData.right, + duplicatedCollectionData.right, ); } } else { @@ -1168,7 +1182,10 @@ export class UserCollectionService { ); } - return E.right(true); + return E.right({ + exportedCollection: JSON.stringify(importedCollectionsWithChildren), + collectionType: reqType, + } as UserCollectionExportJSONData); } /** diff --git a/packages/hoppscotch-backend/src/user-collection/user-collections.model.ts b/packages/hoppscotch-backend/src/user-collection/user-collections.model.ts index b522b37b..c7947c97 100644 --- a/packages/hoppscotch-backend/src/user-collection/user-collections.model.ts +++ b/packages/hoppscotch-backend/src/user-collection/user-collections.model.ts @@ -115,3 +115,16 @@ export class UserCollectionDuplicatedData { }) requests: UserRequest[]; } + +@ObjectType() +export class UserCollectionImportResult { + @Field(() => [UserCollection], { + description: 'Flat array of all collections', + }) + collections: UserCollection[]; + + @Field(() => [UserRequest], { + description: 'Flat array of all requests', + }) + requests: UserRequest[]; +} diff --git a/packages/hoppscotch-common/src/components.d.ts b/packages/hoppscotch-common/src/components.d.ts index 1290b92d..be86b95f 100644 --- a/packages/hoppscotch-common/src/components.d.ts +++ b/packages/hoppscotch-common/src/components.d.ts @@ -298,6 +298,7 @@ declare module 'vue' { MockServerMockServerDashboard: typeof import('./components/mockServer/MockServerDashboard.vue')['default'] MockServerMockServerLogs: typeof import('./components/mockServer/MockServerLogs.vue')['default'] MonacoScriptEditor: typeof import('./components/MonacoScriptEditor.vue')['default'] + Profile: typeof import('./components/profile/index.vue')['default'] ProfileUserDelete: typeof import('./components/profile/UserDelete.vue')['default'] RealtimeCommunication: typeof import('./components/realtime/Communication.vue')['default'] RealtimeConnectionConfig: typeof import('./components/realtime/ConnectionConfig.vue')['default'] @@ -325,6 +326,7 @@ declare module 'vue' { SmartEnvInput: typeof import('./components/smart/EnvInput.vue')['default'] TabPrimary: typeof import('./components/tab/Primary.vue')['default'] TabSecondary: typeof import('./components/tab/Secondary.vue')['default'] + TabsNav: typeof import('./components/TabsNav.vue')['default'] Teams: typeof import('./components/teams/index.vue')['default'] TeamsAdd: typeof import('./components/teams/Add.vue')['default'] TeamsEdit: typeof import('./components/teams/Edit.vue')['default'] @@ -332,6 +334,7 @@ declare module 'vue' { TeamsMemberStack: typeof import('./components/teams/MemberStack.vue')['default'] TeamsModal: typeof import('./components/teams/Modal.vue')['default'] TeamsTeam: typeof import('./components/teams/Team.vue')['default'] + TeamsView: typeof import('./components/teams/View.vue')['default'] Tippy: typeof import('vue-tippy')['Tippy'] WorkspaceCurrent: typeof import('./components/workspace/Current.vue')['default'] WorkspaceSelector: typeof import('./components/workspace/Selector.vue')['default'] diff --git a/packages/hoppscotch-common/src/components/collections/ImportExport.vue b/packages/hoppscotch-common/src/components/collections/ImportExport.vue index e3f80a23..63ad44c4 100644 --- a/packages/hoppscotch-common/src/components/collections/ImportExport.vue +++ b/packages/hoppscotch-common/src/components/collections/ImportExport.vue @@ -34,11 +34,7 @@ import { defineStep } from "~/composables/step-components" import AllCollectionImport from "~/components/importExport/ImportExportSteps/AllCollectionImport.vue" import { useI18n } from "~/composables/i18n" import { useToast } from "~/composables/toast" -import { - appendRESTCollections, - restCollections$, - setRESTCollections, -} from "~/newstore/collections" +import { appendRESTCollections, restCollections$ } from "~/newstore/collections" import IconInsomnia from "~icons/hopp/insomnia" import IconPostman from "~icons/hopp/postman" @@ -51,11 +47,6 @@ import { useReadonlyStream } from "~/composables/stream" import IconUser from "~icons/lucide/user" import { getTeamCollectionJSON } from "~/helpers/backend/helpers" -import { - importUserCollectionsFromJSON, - fetchAndConvertUserCollections, -} from "~/helpers/backend/mutations/UserCollection" -import { ReqType } from "~/helpers/backend/graphql" import { platform } from "~/platform" @@ -68,6 +59,7 @@ import { ImporterOrExporter } from "~/components/importExport/types" import { GistSource } from "~/helpers/import-export/import/import-sources/GistSource" import { TeamWorkspace } from "~/services/workspace.service" import { invokeAction } from "~/helpers/actions" +import { ReqType } from "~/helpers/backend/graphql" const isInsomniaImporterInProgress = ref(false) const isOpenAPIImporterInProgress = ref(false) @@ -121,46 +113,19 @@ const handleImportToStore = async (collections: HoppCollection[]) => { } } -const importToPersonalWorkspace = async (collections: HoppCollection[]) => { - // If user is logged in, try to import to backend first - if (currentUser.value) { - try { - const transformedCollection = collections.map((collection) => - transformCollectionForImport(collection) - ) - - const res = await importUserCollectionsFromJSON( - JSON.stringify(transformedCollection), - ReqType.Rest - )() - - if (E.isRight(res)) { - // Backend import succeeded, now fetch and persist collections in store - const fetchResult = await fetchAndConvertUserCollections(ReqType.Rest) - - if (E.isRight(fetchResult)) { - // Replace local collections with backend collections - setRESTCollections(fetchResult.right) - } else { - // Failed to fetch, append to local store as fallback - appendRESTCollections(collections) - } - - return E.right({ success: true }) - } - // Backend import failed, fall back to local storage - appendRESTCollections(collections) - return E.right({ success: true }) - } catch { - // Backend import failed, fall back to local storage - appendRESTCollections(collections) - return E.right({ success: true }) - } - } else { - // User not logged in, use local storage - appendRESTCollections(collections) - return E.right({ success: true }) +const importToPersonalWorkspace = (collections: HoppCollection[]) => { + if ( + platform.sync.collections.importToPersonalWorkspace && + currentUser.value + ) { + return platform.sync.collections.importToPersonalWorkspace( + collections, + ReqType.Rest + ) } + + appendRESTCollections(collections) + return E.right({ success: true }) } const importToTeamsWorkspace = async (collections: HoppCollection[]) => { diff --git a/packages/hoppscotch-common/src/components/collections/graphql/ImportExport.vue b/packages/hoppscotch-common/src/components/collections/graphql/ImportExport.vue index 58d54ff5..08f85704 100644 --- a/packages/hoppscotch-common/src/components/collections/graphql/ImportExport.vue +++ b/packages/hoppscotch-common/src/components/collections/graphql/ImportExport.vue @@ -12,18 +12,12 @@ import { HoppCollection } from "@hoppscotch/data" import * as E from "fp-ts/Either" import { ref } from "vue" -import { transformCollectionForImport } from "~/helpers/collection/collection" import { useI18n } from "~/composables/i18n" import { useToast } from "~/composables/toast" import { ImporterOrExporter } from "~/components/importExport/types" import { FileSource } from "~/helpers/import-export/import/import-sources/FileSource" import { GistSource } from "~/helpers/import-export/import/import-sources/GistSource" -import { - importUserCollectionsFromJSON, - fetchAndConvertUserCollections, -} from "~/helpers/backend/mutations/UserCollection" -import { ReqType } from "~/helpers/backend/graphql" import IconFolderPlus from "~icons/lucide/folder-plus" import IconUser from "~icons/lucide/user" @@ -34,13 +28,13 @@ import { platform } from "~/platform" import { appendGraphqlCollections, graphqlCollections$, - setGraphqlCollections, } from "~/newstore/collections" import { hoppGqlCollectionsImporter } from "~/helpers/import-export/import/hoppGql" import { gqlCollectionsExporter } from "~/helpers/import-export/export/gqlCollections" import { gistExporter } from "~/helpers/import-export/export/gist" import { computed } from "vue" import { hoppGQLImporter } from "~/helpers/import-export/import/hopp" +import { ReqType } from "~/helpers/backend/graphql" const t = useI18n() const toast = useToast() @@ -238,49 +232,19 @@ const showImportFailedError = () => { toast.error(t("import.failed")) } -const handleImportToStore = async (gqlCollections: HoppCollection[]) => { - // If user is logged in, try to import to backend first - if (currentUser.value) { - try { - const transformedCollection = gqlCollections.map((collection) => - transformCollectionForImport(collection) - ) - - const res = await importUserCollectionsFromJSON( - JSON.stringify(transformedCollection), - ReqType.Gql - )() - - if (E.isRight(res)) { - // Backend import succeeded, now fetch and persist collections in store - const fetchResult = await fetchAndConvertUserCollections(ReqType.Gql) - - if (E.isRight(fetchResult)) { - // Replace local collections with backend collections - setGraphqlCollections(fetchResult.right) - } else { - // Failed to fetch, append to local store as fallback - appendGraphqlCollections(gqlCollections) - } - - toast.success(t("state.file_imported")) - return - } - // Backend import failed, fall back to local storage - appendGraphqlCollections(gqlCollections) - toast.success(t("state.file_imported")) - return - } catch { - // Backend import failed, fall back to local storage - appendGraphqlCollections(gqlCollections) - toast.success(t("state.file_imported")) - return - } - } else { - // User not logged in, use local storage - appendGraphqlCollections(gqlCollections) - toast.success(t("state.file_imported")) +const handleImportToStore = (gqlCollections: HoppCollection[]) => { + if ( + platform.sync.collections.importToPersonalWorkspace && + currentUser.value + ) { + return platform.sync.collections.importToPersonalWorkspace( + gqlCollections, + ReqType.Gql + ) } + + appendGraphqlCollections(gqlCollections) + toast.success(t("state.file_imported")) } const emit = defineEmits<{ diff --git a/packages/hoppscotch-common/src/helpers/backend/gql/mutations/ImportUserCollectionsFromJSON.graphql b/packages/hoppscotch-common/src/helpers/backend/gql/mutations/ImportUserCollectionsFromJSON.graphql deleted file mode 100644 index eced5dc4..00000000 --- a/packages/hoppscotch-common/src/helpers/backend/gql/mutations/ImportUserCollectionsFromJSON.graphql +++ /dev/null @@ -1,11 +0,0 @@ -mutation ImportUserCollectionsFromJSON( - $jsonString: String! - $reqType: ReqType! - $parentCollectionID: ID -) { - importUserCollectionsFromJSON( - jsonString: $jsonString - reqType: $reqType - parentCollectionID: $parentCollectionID - ) -} \ No newline at end of file diff --git a/packages/hoppscotch-common/src/helpers/backend/gql/queries/GetUserRootCollections.graphql b/packages/hoppscotch-common/src/helpers/backend/gql/queries/GetUserRootCollections.graphql deleted file mode 100644 index 40c3f22e..00000000 --- a/packages/hoppscotch-common/src/helpers/backend/gql/queries/GetUserRootCollections.graphql +++ /dev/null @@ -1,34 +0,0 @@ -query GetUserRootCollections($cursor: ID, $take: Int) { - rootRESTUserCollections(cursor: $cursor, take: $take) { - id - title - data - type - parent { - id - } - requests { - id - title - request - type - collectionID - } - childrenREST { - id - title - data - type - parent { - id - } - requests { - id - title - request - type - collectionID - } - } - } -} \ No newline at end of file diff --git a/packages/hoppscotch-common/src/helpers/backend/mutations/UserCollection.ts b/packages/hoppscotch-common/src/helpers/backend/mutations/UserCollection.ts deleted file mode 100644 index 24c3ca82..00000000 --- a/packages/hoppscotch-common/src/helpers/backend/mutations/UserCollection.ts +++ /dev/null @@ -1,200 +0,0 @@ -import { runMutation } from "../GQLClient" -import { runGQLQuery } from "../GQLClient" -import { - GetGqlRootUserCollectionsDocument, - GetGqlRootUserCollectionsQuery, - GetGqlRootUserCollectionsQueryVariables, - GetUserRootCollectionsDocument, - GetUserRootCollectionsQuery, - GetUserRootCollectionsQueryVariables, - ImportUserCollectionsFromJsonDocument, - ImportUserCollectionsFromJsonMutation, - ImportUserCollectionsFromJsonMutationVariables, - ReqType, - UserCollection, - UserRequest, -} from "../graphql" -import { - HoppCollection, - makeCollection, - HoppRESTRequest, - HoppGQLRequest, - getDefaultRESTRequest, - getDefaultGQLRequest, -} from "@hoppscotch/data" -import * as E from "fp-ts/Either" - -export const importUserCollectionsFromJSON = ( - collectionJSON: string, - reqType: ReqType, - parentCollectionID?: string -) => - runMutation< - ImportUserCollectionsFromJsonMutation, - ImportUserCollectionsFromJsonMutationVariables, - "" - >(ImportUserCollectionsFromJsonDocument, { - jsonString: collectionJSON, - reqType, - parentCollectionID, - }) - -// Use generated GraphQL documents instead of inline gql tags -export const getUserRootCollections = () => - runGQLQuery< - GetUserRootCollectionsQuery, - GetUserRootCollectionsQueryVariables, - "" - >({ - query: GetUserRootCollectionsDocument, - variables: {}, - }) - -export const getGQLRootUserCollections = () => - runGQLQuery< - GetGqlRootUserCollectionsQuery, - GetGqlRootUserCollectionsQueryVariables, - "" - >({ - query: GetGqlRootUserCollectionsDocument, - variables: {}, - }) - -/** - * Converts a UserRequest from backend format to HoppRequest format - */ -function convertUserRequestToHoppRequest( - userRequest: UserRequest -): HoppRESTRequest | HoppGQLRequest { - try { - const parsedRequest = JSON.parse(userRequest.request) - - // Add the backend ID and title to the request - const request = { - ...parsedRequest, - id: userRequest.id, - name: userRequest.title, - } - - return request - } catch { - // Return a default request if parsing fails - if (userRequest.type === ReqType.Rest) { - const defaultRequest = getDefaultRESTRequest() - defaultRequest.id = userRequest.id - defaultRequest.name = userRequest.title - return defaultRequest - } - const defaultRequest = getDefaultGQLRequest() - defaultRequest.id = userRequest.id - defaultRequest.name = userRequest.title - return defaultRequest - } -} - -/** - * Parse collection data similar to the existing parseCollectionData function in helpers.ts - */ -function parseUserCollectionData(data: string | null | undefined) { - const defaultDataProps = { - auth: { authType: "inherit", authActive: true }, - headers: [], - variables: [], - } - - if (!data) { - return defaultDataProps - } - - try { - const parsedData = JSON.parse(data) - return { - auth: parsedData?.auth || defaultDataProps.auth, - headers: parsedData?.headers || defaultDataProps.headers, - variables: parsedData?.variables || defaultDataProps.variables, - } - } catch { - return defaultDataProps - } -} - -/** - * Converts a UserCollection from backend format to HoppCollection format - * Following the same pattern as teamCollectionJSONToHoppRESTColl in helpers.ts - */ -export function convertUserCollectionToHoppCollection( - userCollection: UserCollection, - reqType: ReqType -): HoppCollection { - const { auth, headers, variables } = parseUserCollectionData( - userCollection.data - ) - - // Get the appropriate children based on request type - const children = - reqType === ReqType.Rest - ? userCollection.childrenREST - : userCollection.childrenGQL - - // Convert requests - filter by type and convert - const requests = userCollection.requests - ? userCollection.requests - .filter((req) => req.type === reqType) - .map(convertUserRequestToHoppRequest) - : [] - - const collection = makeCollection({ - name: userCollection.title, - folders: children - ? children.map((child) => - convertUserCollectionToHoppCollection(child, reqType) - ) - : [], - requests: requests, - auth, - headers, - variables, - }) - - // Add the backend ID to the collection - collection.id = userCollection.id - - return collection -} - -/** - * Fetches user collections from backend and converts them to HoppCollection format - */ -export const fetchAndConvertUserCollections = async (reqType: ReqType) => { - const fetchFunction = - reqType === ReqType.Rest - ? getUserRootCollections - : getGQLRootUserCollections - - const result = await fetchFunction() - - if (E.isLeft(result)) { - return E.left(result.left) - } - - if (reqType === ReqType.Rest) { - const right = result.right as GetUserRootCollectionsQuery - const collections = right.rootRESTUserCollections - const convertedCollections = collections.map((collection) => - convertUserCollectionToHoppCollection( - collection as unknown as UserCollection, - reqType - ) - ) - return E.right(convertedCollections) - } - const right = result.right as GetGqlRootUserCollectionsQuery - const collections = right.rootGQLUserCollections - const convertedCollections = collections.map((collection) => - convertUserCollectionToHoppCollection( - collection as unknown as UserCollection, - reqType - ) - ) - return E.right(convertedCollections) -} diff --git a/packages/hoppscotch-common/src/platform/collections.ts b/packages/hoppscotch-common/src/platform/collections.ts index 107ba4af..4786a792 100644 --- a/packages/hoppscotch-common/src/platform/collections.ts +++ b/packages/hoppscotch-common/src/platform/collections.ts @@ -1,3 +1,11 @@ +import { HoppCollection } from "@hoppscotch/data" +import { ReqType } from "~/helpers/backend/graphql" +import * as E from "fp-ts/Either" + export type CollectionsPlatformDef = { initCollectionsSync: () => void + importToPersonalWorkspace?: ( + collections: HoppCollection[], + reqType: ReqType + ) => Promise> } diff --git a/packages/hoppscotch-selfhost-web/src/api/mutations/ImportUserCollectionsFromJSON.graphql b/packages/hoppscotch-selfhost-web/src/api/mutations/ImportUserCollectionsFromJSON.graphql index 0fa6a920..beddb872 100644 --- a/packages/hoppscotch-selfhost-web/src/api/mutations/ImportUserCollectionsFromJSON.graphql +++ b/packages/hoppscotch-selfhost-web/src/api/mutations/ImportUserCollectionsFromJSON.graphql @@ -7,5 +7,8 @@ mutation ImportUserCollectionsFromJSON( jsonString: $jsonString reqType: $reqType parentCollectionID: $parentCollectionID - ) + ) { + exportedCollection + collectionType + } } diff --git a/packages/hoppscotch-selfhost-web/src/platform/collections/desktop/api.ts b/packages/hoppscotch-selfhost-web/src/platform/collections/desktop/api.ts index e06cbf51..697722e6 100644 --- a/packages/hoppscotch-selfhost-web/src/platform/collections/desktop/api.ts +++ b/packages/hoppscotch-selfhost-web/src/platform/collections/desktop/api.ts @@ -1,8 +1,3 @@ -import { - runGQLQuery, - runGQLSubscription, - runMutation, -} from "@helpers/backend/GQLClient" import { CreateGqlChildUserCollectionDocument, CreateGqlChildUserCollectionMutation, @@ -34,12 +29,6 @@ import { ExportUserCollectionsToJsonDocument, ExportUserCollectionsToJsonQuery, ExportUserCollectionsToJsonQueryVariables, - GetGqlRootUserCollectionsDocument, - GetGqlRootUserCollectionsQuery, - GetGqlRootUserCollectionsQueryVariables, - GetUserRootCollectionsDocument, - GetUserRootCollectionsQuery, - GetUserRootCollectionsQueryVariables, ImportUserCollectionsFromJsonDocument, ImportUserCollectionsFromJsonMutation, ImportUserCollectionsFromJsonMutationVariables, @@ -82,6 +71,11 @@ import { UserRequestUpdatedDocument, UserRootCollectionsSortedDocument, } from "@app/api/generated/graphql" +import { + runGQLQuery, + runGQLSubscription, + runMutation, +} from "@hoppscotch/common/helpers/backend/GQLClient" export const createRESTRootUserCollection = (title: string, data?: string) => runMutation< @@ -300,26 +294,6 @@ export const updateUserCollectionOrder = ( 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 diff --git a/packages/hoppscotch-selfhost-web/src/platform/collections/desktop/import.ts b/packages/hoppscotch-selfhost-web/src/platform/collections/desktop/import.ts new file mode 100644 index 00000000..269636ce --- /dev/null +++ b/packages/hoppscotch-selfhost-web/src/platform/collections/desktop/import.ts @@ -0,0 +1 @@ +export { importToPersonalWorkspace } from "../web/import" diff --git a/packages/hoppscotch-selfhost-web/src/platform/collections/desktop/index.ts b/packages/hoppscotch-selfhost-web/src/platform/collections/desktop/index.ts index 91899540..4f74fc4a 100644 --- a/packages/hoppscotch-selfhost-web/src/platform/collections/desktop/index.ts +++ b/packages/hoppscotch-selfhost-web/src/platform/collections/desktop/index.ts @@ -98,7 +98,7 @@ function initCollectionsSync() { }) } -type ExportedUserCollectionREST = { +export type ExportedUserCollectionREST = { id?: string _ref_id?: string folders: ExportedUserCollectionREST[] @@ -107,7 +107,7 @@ type ExportedUserCollectionREST = { data: string } -type ExportedUserCollectionGQL = { +export type ExportedUserCollectionGQL = { id?: string _ref_id?: string folders: ExportedUserCollectionGQL[] @@ -125,7 +125,7 @@ function addDescriptionField( })) } -function exportedCollectionToHoppCollection( +export function exportedCollectionToHoppCollection( collection: ExportedUserCollectionREST | ExportedUserCollectionGQL, collectionType: "REST" | "GQL" ): HoppCollection { @@ -1028,8 +1028,11 @@ function setupUserRequestDeletedSubscription() { return userRequestDeletedSub } +import { importToPersonalWorkspace } from "./import" + export const def: CollectionsPlatformDef = { initCollectionsSync, + importToPersonalWorkspace, } function getCollectionPathFromCollectionID( diff --git a/packages/hoppscotch-selfhost-web/src/platform/collections/web/api.ts b/packages/hoppscotch-selfhost-web/src/platform/collections/web/api.ts index e06cbf51..697722e6 100644 --- a/packages/hoppscotch-selfhost-web/src/platform/collections/web/api.ts +++ b/packages/hoppscotch-selfhost-web/src/platform/collections/web/api.ts @@ -1,8 +1,3 @@ -import { - runGQLQuery, - runGQLSubscription, - runMutation, -} from "@helpers/backend/GQLClient" import { CreateGqlChildUserCollectionDocument, CreateGqlChildUserCollectionMutation, @@ -34,12 +29,6 @@ import { ExportUserCollectionsToJsonDocument, ExportUserCollectionsToJsonQuery, ExportUserCollectionsToJsonQueryVariables, - GetGqlRootUserCollectionsDocument, - GetGqlRootUserCollectionsQuery, - GetGqlRootUserCollectionsQueryVariables, - GetUserRootCollectionsDocument, - GetUserRootCollectionsQuery, - GetUserRootCollectionsQueryVariables, ImportUserCollectionsFromJsonDocument, ImportUserCollectionsFromJsonMutation, ImportUserCollectionsFromJsonMutationVariables, @@ -82,6 +71,11 @@ import { UserRequestUpdatedDocument, UserRootCollectionsSortedDocument, } from "@app/api/generated/graphql" +import { + runGQLQuery, + runGQLSubscription, + runMutation, +} from "@hoppscotch/common/helpers/backend/GQLClient" export const createRESTRootUserCollection = (title: string, data?: string) => runMutation< @@ -300,26 +294,6 @@ export const updateUserCollectionOrder = ( 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 diff --git a/packages/hoppscotch-selfhost-web/src/platform/collections/web/import.ts b/packages/hoppscotch-selfhost-web/src/platform/collections/web/import.ts new file mode 100644 index 00000000..a3bdf1e8 --- /dev/null +++ b/packages/hoppscotch-selfhost-web/src/platform/collections/web/import.ts @@ -0,0 +1,112 @@ +import { ReqType } from "@hoppscotch/common/helpers/backend/graphql" +import { + appendGraphqlCollections, + appendRESTCollections, +} from "@hoppscotch/common/newstore/collections" +import { HoppCollection } from "@hoppscotch/data" +import * as E from "fp-ts/Either" +import { + exportedCollectionToHoppCollection, + ExportedUserCollectionGQL, + ExportedUserCollectionREST, +} from "../web" +import { importUserCollectionsFromJSON } from "./api" +import { runDispatchWithOutSyncing } from "@app/lib/sync" + +/** + * Platform-specific import function for selfhost-web that uses the correct nested collection queries + */ +export const importToPersonalWorkspace = async ( + collections: HoppCollection[], + reqType: ReqType +) => { + try { + const transformedCollection = collections.map((collection) => + translateToPersonalCollectionFormat(collection) + ) + + const res = await importUserCollectionsFromJSON( + JSON.stringify(transformedCollection), + reqType + ) + + if (E.isRight(res)) { + await loadImportedUserCollections( + res.right.importUserCollectionsFromJSON.exportedCollection, + res.right.importUserCollectionsFromJSON.collectionType === "REST" + ? "REST" + : "GQL" + ) + return E.right({ success: true }) + } + // Backend import failed, fall back to local storage + return appendCollectionsToStore(collections, reqType) + } catch { + // On any error, fall back to local storage + return appendCollectionsToStore(collections, reqType) + } +} + +export const appendCollectionsToStore = ( + collections: HoppCollection[], + reqType: ReqType +) => { + if (reqType === ReqType.Rest) { + appendRESTCollections(collections) + } else { + appendGraphqlCollections(collections) + } + return E.right({ success: true }) +} + +export function translateToPersonalCollectionFormat(x: HoppCollection) { + const folders: HoppCollection[] = (x.folders ?? []).map( + translateToPersonalCollectionFormat + ) + + const data = { + auth: x.auth, + headers: x.headers, + variables: x.variables, + } + + const obj = { + ...x, + folders, + data, + } + + return obj +} + +export async function loadImportedUserCollections( + collectionsJSONString: string, + collectionType: "REST" | "GQL" +) { + const importedCollections = ( + JSON.parse(collectionsJSONString) as Array< + ExportedUserCollectionGQL | ExportedUserCollectionREST + > + ).map((collection) => ({ v: 1, ...collection })) + runDispatchWithOutSyncing(() => { + collectionType === "REST" + ? appendRESTCollections( + importedCollections.map( + (collection) => + exportedCollectionToHoppCollection( + collection, + "REST" + ) as HoppCollection + ) + ) + : appendGraphqlCollections( + importedCollections.map( + (collection) => + exportedCollectionToHoppCollection( + collection, + "GQL" + ) as HoppCollection + ) + ) + }) +} diff --git a/packages/hoppscotch-selfhost-web/src/platform/collections/web/index.ts b/packages/hoppscotch-selfhost-web/src/platform/collections/web/index.ts index 938764a0..af53ef8b 100644 --- a/packages/hoppscotch-selfhost-web/src/platform/collections/web/index.ts +++ b/packages/hoppscotch-selfhost-web/src/platform/collections/web/index.ts @@ -67,6 +67,7 @@ import { } from "@hoppscotch/data" import * as E from "fp-ts/Either" import { gqlCollectionsSyncer } from "./gqlCollections.sync" +import { importToPersonalWorkspace } from "./import" function initCollectionsSync() { const currentUser$ = platformAuth.getCurrentUserStream() @@ -98,7 +99,7 @@ function initCollectionsSync() { }) } -type ExportedUserCollectionREST = { +export type ExportedUserCollectionREST = { id?: string _ref_id?: string folders: ExportedUserCollectionREST[] @@ -107,7 +108,7 @@ type ExportedUserCollectionREST = { data: string } -type ExportedUserCollectionGQL = { +export type ExportedUserCollectionGQL = { id?: string _ref_id?: string folders: ExportedUserCollectionGQL[] @@ -125,7 +126,7 @@ function addDescriptionField( })) } -function exportedCollectionToHoppCollection( +export function exportedCollectionToHoppCollection( collection: ExportedUserCollectionREST | ExportedUserCollectionGQL, collectionType: "REST" | "GQL" ): HoppCollection { @@ -1031,6 +1032,7 @@ function setupUserRequestDeletedSubscription() { export const def: CollectionsPlatformDef = { initCollectionsSync, + importToPersonalWorkspace, } function getCollectionPathFromCollectionID(