feat: add platform-specific import support for personal collections (#5570)

Co-authored-by: mirarifhasan <arif.ishan05@gmail.com>
Co-authored-by: James George <25279263+jamesgeorge007@users.noreply.github.com>
This commit is contained in:
Anwarul Islam 2025-11-25 22:33:21 +06:00 committed by GitHub
parent 77af577778
commit 03212386fb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 214 additions and 419 deletions

View file

@ -18,6 +18,7 @@ import {
UserCollection, UserCollection,
UserCollectionDuplicatedData, UserCollectionDuplicatedData,
UserCollectionExportJSONData, UserCollectionExportJSONData,
UserCollectionImportResult,
UserCollectionRemovedData, UserCollectionRemovedData,
UserCollectionReorderData, UserCollectionReorderData,
} from './user-collections.model'; } from './user-collections.model';
@ -375,7 +376,7 @@ export class UserCollectionResolver {
return res.right; return res.right;
} }
@Mutation(() => Boolean, { @Mutation(() => UserCollectionExportJSONData, {
description: 'Import collections from JSON string to the specified Team', description: 'Import collections from JSON string to the specified Team',
}) })
@UseGuards(GqlAuthGuard) @UseGuards(GqlAuthGuard)

View file

@ -40,6 +40,7 @@ import {
import { CollectionFolder } from 'src/types/CollectionFolder'; import { CollectionFolder } from 'src/types/CollectionFolder';
import { PrismaError } from 'src/prisma/prisma-error-codes'; import { PrismaError } from 'src/prisma/prisma-error-codes';
import { SortOptions } from 'src/types/SortOptions'; import { SortOptions } from 'src/types/SortOptions';
import { UserRequest } from 'src/user-request/user-request.model';
@Injectable() @Injectable()
export class UserCollectionService { export class UserCollectionService {
@ -970,7 +971,7 @@ export class UserCollectionService {
collectionListObjects.push(result.right); 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) { if (collectionID) {
// Get Details of collection // Get Details of collection
const parentCollection = await this.getUserCollection(collectionID); const parentCollection = await this.getUserCollection(collectionID);
@ -1033,7 +1034,10 @@ export class UserCollectionService {
let data = null; let data = null;
if (folder.data) { if (folder.data) {
try { try {
data = JSON.parse(folder.data); data =
typeof folder.data === 'string'
? JSON.parse(folder.data)
: folder.data;
} catch (error) { } catch (error) {
// If data parsing fails, log error and continue without data // If data parsing fails, log error and continue without data
console.error('Failed to parse collection data:', error); console.error('Failed to parse collection data:', error);
@ -1149,14 +1153,24 @@ export class UserCollectionService {
return E.left(USER_COLLECTION_CREATION_FAILED); 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) { if (isCollectionDuplication) {
const collectionData = await this.fetchCollectionData( const duplicatedCollectionData = await this.fetchCollectionData(
userCollections[0].id, userCollections[0].id,
); );
if (E.isRight(collectionData)) { if (E.isRight(duplicatedCollectionData)) {
this.pubsub.publish( this.pubsub.publish(
`user_coll/${userID}/duplicated`, `user_coll/${userID}/duplicated`,
collectionData.right, duplicatedCollectionData.right,
); );
} }
} else { } else {
@ -1168,7 +1182,10 @@ export class UserCollectionService {
); );
} }
return E.right(true); return E.right({
exportedCollection: JSON.stringify(importedCollectionsWithChildren),
collectionType: reqType,
} as UserCollectionExportJSONData);
} }
/** /**

View file

@ -115,3 +115,16 @@ export class UserCollectionDuplicatedData {
}) })
requests: UserRequest[]; 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[];
}

View file

@ -298,6 +298,7 @@ declare module 'vue' {
MockServerMockServerDashboard: typeof import('./components/mockServer/MockServerDashboard.vue')['default'] MockServerMockServerDashboard: typeof import('./components/mockServer/MockServerDashboard.vue')['default']
MockServerMockServerLogs: typeof import('./components/mockServer/MockServerLogs.vue')['default'] MockServerMockServerLogs: typeof import('./components/mockServer/MockServerLogs.vue')['default']
MonacoScriptEditor: typeof import('./components/MonacoScriptEditor.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'] ProfileUserDelete: typeof import('./components/profile/UserDelete.vue')['default']
RealtimeCommunication: typeof import('./components/realtime/Communication.vue')['default'] RealtimeCommunication: typeof import('./components/realtime/Communication.vue')['default']
RealtimeConnectionConfig: typeof import('./components/realtime/ConnectionConfig.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'] SmartEnvInput: typeof import('./components/smart/EnvInput.vue')['default']
TabPrimary: typeof import('./components/tab/Primary.vue')['default'] TabPrimary: typeof import('./components/tab/Primary.vue')['default']
TabSecondary: typeof import('./components/tab/Secondary.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'] Teams: typeof import('./components/teams/index.vue')['default']
TeamsAdd: typeof import('./components/teams/Add.vue')['default'] TeamsAdd: typeof import('./components/teams/Add.vue')['default']
TeamsEdit: typeof import('./components/teams/Edit.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'] TeamsMemberStack: typeof import('./components/teams/MemberStack.vue')['default']
TeamsModal: typeof import('./components/teams/Modal.vue')['default'] TeamsModal: typeof import('./components/teams/Modal.vue')['default']
TeamsTeam: typeof import('./components/teams/Team.vue')['default'] TeamsTeam: typeof import('./components/teams/Team.vue')['default']
TeamsView: typeof import('./components/teams/View.vue')['default']
Tippy: typeof import('vue-tippy')['Tippy'] Tippy: typeof import('vue-tippy')['Tippy']
WorkspaceCurrent: typeof import('./components/workspace/Current.vue')['default'] WorkspaceCurrent: typeof import('./components/workspace/Current.vue')['default']
WorkspaceSelector: typeof import('./components/workspace/Selector.vue')['default'] WorkspaceSelector: typeof import('./components/workspace/Selector.vue')['default']

View file

@ -34,11 +34,7 @@ import { defineStep } from "~/composables/step-components"
import AllCollectionImport from "~/components/importExport/ImportExportSteps/AllCollectionImport.vue" import AllCollectionImport from "~/components/importExport/ImportExportSteps/AllCollectionImport.vue"
import { useI18n } from "~/composables/i18n" import { useI18n } from "~/composables/i18n"
import { useToast } from "~/composables/toast" import { useToast } from "~/composables/toast"
import { import { appendRESTCollections, restCollections$ } from "~/newstore/collections"
appendRESTCollections,
restCollections$,
setRESTCollections,
} from "~/newstore/collections"
import IconInsomnia from "~icons/hopp/insomnia" import IconInsomnia from "~icons/hopp/insomnia"
import IconPostman from "~icons/hopp/postman" import IconPostman from "~icons/hopp/postman"
@ -51,11 +47,6 @@ import { useReadonlyStream } from "~/composables/stream"
import IconUser from "~icons/lucide/user" import IconUser from "~icons/lucide/user"
import { getTeamCollectionJSON } from "~/helpers/backend/helpers" import { getTeamCollectionJSON } from "~/helpers/backend/helpers"
import {
importUserCollectionsFromJSON,
fetchAndConvertUserCollections,
} from "~/helpers/backend/mutations/UserCollection"
import { ReqType } from "~/helpers/backend/graphql"
import { platform } from "~/platform" 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 { GistSource } from "~/helpers/import-export/import/import-sources/GistSource"
import { TeamWorkspace } from "~/services/workspace.service" import { TeamWorkspace } from "~/services/workspace.service"
import { invokeAction } from "~/helpers/actions" import { invokeAction } from "~/helpers/actions"
import { ReqType } from "~/helpers/backend/graphql"
const isInsomniaImporterInProgress = ref(false) const isInsomniaImporterInProgress = ref(false)
const isOpenAPIImporterInProgress = ref(false) const isOpenAPIImporterInProgress = ref(false)
@ -121,46 +113,19 @@ const handleImportToStore = async (collections: HoppCollection[]) => {
} }
} }
const importToPersonalWorkspace = async (collections: HoppCollection[]) => { const importToPersonalWorkspace = (collections: HoppCollection[]) => {
// If user is logged in, try to import to backend first if (
if (currentUser.value) { platform.sync.collections.importToPersonalWorkspace &&
try { currentUser.value
const transformedCollection = collections.map((collection) => ) {
transformCollectionForImport(collection) return platform.sync.collections.importToPersonalWorkspace(
) collections,
ReqType.Rest
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 })
} }
appendRESTCollections(collections)
return E.right({ success: true })
} }
const importToTeamsWorkspace = async (collections: HoppCollection[]) => { const importToTeamsWorkspace = async (collections: HoppCollection[]) => {

View file

@ -12,18 +12,12 @@
import { HoppCollection } from "@hoppscotch/data" import { HoppCollection } from "@hoppscotch/data"
import * as E from "fp-ts/Either" import * as E from "fp-ts/Either"
import { ref } from "vue" import { ref } from "vue"
import { transformCollectionForImport } from "~/helpers/collection/collection"
import { useI18n } from "~/composables/i18n" import { useI18n } from "~/composables/i18n"
import { useToast } from "~/composables/toast" import { useToast } from "~/composables/toast"
import { ImporterOrExporter } from "~/components/importExport/types" import { ImporterOrExporter } from "~/components/importExport/types"
import { FileSource } from "~/helpers/import-export/import/import-sources/FileSource" import { FileSource } from "~/helpers/import-export/import/import-sources/FileSource"
import { GistSource } from "~/helpers/import-export/import/import-sources/GistSource" 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 IconFolderPlus from "~icons/lucide/folder-plus"
import IconUser from "~icons/lucide/user" import IconUser from "~icons/lucide/user"
@ -34,13 +28,13 @@ import { platform } from "~/platform"
import { import {
appendGraphqlCollections, appendGraphqlCollections,
graphqlCollections$, graphqlCollections$,
setGraphqlCollections,
} from "~/newstore/collections" } from "~/newstore/collections"
import { hoppGqlCollectionsImporter } from "~/helpers/import-export/import/hoppGql" import { hoppGqlCollectionsImporter } from "~/helpers/import-export/import/hoppGql"
import { gqlCollectionsExporter } from "~/helpers/import-export/export/gqlCollections" import { gqlCollectionsExporter } from "~/helpers/import-export/export/gqlCollections"
import { gistExporter } from "~/helpers/import-export/export/gist" import { gistExporter } from "~/helpers/import-export/export/gist"
import { computed } from "vue" import { computed } from "vue"
import { hoppGQLImporter } from "~/helpers/import-export/import/hopp" import { hoppGQLImporter } from "~/helpers/import-export/import/hopp"
import { ReqType } from "~/helpers/backend/graphql"
const t = useI18n() const t = useI18n()
const toast = useToast() const toast = useToast()
@ -238,49 +232,19 @@ const showImportFailedError = () => {
toast.error(t("import.failed")) toast.error(t("import.failed"))
} }
const handleImportToStore = async (gqlCollections: HoppCollection[]) => { const handleImportToStore = (gqlCollections: HoppCollection[]) => {
// If user is logged in, try to import to backend first if (
if (currentUser.value) { platform.sync.collections.importToPersonalWorkspace &&
try { currentUser.value
const transformedCollection = gqlCollections.map((collection) => ) {
transformCollectionForImport(collection) return platform.sync.collections.importToPersonalWorkspace(
) gqlCollections,
ReqType.Gql
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"))
} }
appendGraphqlCollections(gqlCollections)
toast.success(t("state.file_imported"))
} }
const emit = defineEmits<{ const emit = defineEmits<{

View file

@ -1,11 +0,0 @@
mutation ImportUserCollectionsFromJSON(
$jsonString: String!
$reqType: ReqType!
$parentCollectionID: ID
) {
importUserCollectionsFromJSON(
jsonString: $jsonString
reqType: $reqType
parentCollectionID: $parentCollectionID
)
}

View file

@ -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
}
}
}
}

View file

@ -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)
}

View file

@ -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 = { export type CollectionsPlatformDef = {
initCollectionsSync: () => void initCollectionsSync: () => void
importToPersonalWorkspace?: (
collections: HoppCollection[],
reqType: ReqType
) => Promise<E.Either<string, { success: boolean }>>
} }

View file

@ -7,5 +7,8 @@ mutation ImportUserCollectionsFromJSON(
jsonString: $jsonString jsonString: $jsonString
reqType: $reqType reqType: $reqType
parentCollectionID: $parentCollectionID parentCollectionID: $parentCollectionID
) ) {
exportedCollection
collectionType
}
} }

View file

@ -1,8 +1,3 @@
import {
runGQLQuery,
runGQLSubscription,
runMutation,
} from "@helpers/backend/GQLClient"
import { import {
CreateGqlChildUserCollectionDocument, CreateGqlChildUserCollectionDocument,
CreateGqlChildUserCollectionMutation, CreateGqlChildUserCollectionMutation,
@ -34,12 +29,6 @@ import {
ExportUserCollectionsToJsonDocument, ExportUserCollectionsToJsonDocument,
ExportUserCollectionsToJsonQuery, ExportUserCollectionsToJsonQuery,
ExportUserCollectionsToJsonQueryVariables, ExportUserCollectionsToJsonQueryVariables,
GetGqlRootUserCollectionsDocument,
GetGqlRootUserCollectionsQuery,
GetGqlRootUserCollectionsQueryVariables,
GetUserRootCollectionsDocument,
GetUserRootCollectionsQuery,
GetUserRootCollectionsQueryVariables,
ImportUserCollectionsFromJsonDocument, ImportUserCollectionsFromJsonDocument,
ImportUserCollectionsFromJsonMutation, ImportUserCollectionsFromJsonMutation,
ImportUserCollectionsFromJsonMutationVariables, ImportUserCollectionsFromJsonMutationVariables,
@ -82,6 +71,11 @@ import {
UserRequestUpdatedDocument, UserRequestUpdatedDocument,
UserRootCollectionsSortedDocument, UserRootCollectionsSortedDocument,
} from "@app/api/generated/graphql" } from "@app/api/generated/graphql"
import {
runGQLQuery,
runGQLSubscription,
runMutation,
} from "@hoppscotch/common/helpers/backend/GQLClient"
export const createRESTRootUserCollection = (title: string, data?: string) => export const createRESTRootUserCollection = (title: string, data?: string) =>
runMutation< runMutation<
@ -300,26 +294,6 @@ export const updateUserCollectionOrder = (
nextCollectionID, nextCollectionID,
})() })()
export const getUserRootCollections = () =>
runGQLQuery<
GetUserRootCollectionsQuery,
GetUserRootCollectionsQueryVariables,
""
>({
query: GetUserRootCollectionsDocument,
variables: {},
})
export const getGQLRootUserCollections = () =>
runGQLQuery<
GetGqlRootUserCollectionsQuery,
GetGqlRootUserCollectionsQueryVariables,
""
>({
query: GetGqlRootUserCollectionsDocument,
variables: {},
})
export const exportUserCollectionsToJSON = ( export const exportUserCollectionsToJSON = (
collectionID?: string, collectionID?: string,
collectionType: ReqType.Rest | ReqType.Gql = ReqType.Rest collectionType: ReqType.Rest | ReqType.Gql = ReqType.Rest

View file

@ -0,0 +1 @@
export { importToPersonalWorkspace } from "../web/import"

View file

@ -98,7 +98,7 @@ function initCollectionsSync() {
}) })
} }
type ExportedUserCollectionREST = { export type ExportedUserCollectionREST = {
id?: string id?: string
_ref_id?: string _ref_id?: string
folders: ExportedUserCollectionREST[] folders: ExportedUserCollectionREST[]
@ -107,7 +107,7 @@ type ExportedUserCollectionREST = {
data: string data: string
} }
type ExportedUserCollectionGQL = { export type ExportedUserCollectionGQL = {
id?: string id?: string
_ref_id?: string _ref_id?: string
folders: ExportedUserCollectionGQL[] folders: ExportedUserCollectionGQL[]
@ -125,7 +125,7 @@ function addDescriptionField(
})) }))
} }
function exportedCollectionToHoppCollection( export function exportedCollectionToHoppCollection(
collection: ExportedUserCollectionREST | ExportedUserCollectionGQL, collection: ExportedUserCollectionREST | ExportedUserCollectionGQL,
collectionType: "REST" | "GQL" collectionType: "REST" | "GQL"
): HoppCollection { ): HoppCollection {
@ -1028,8 +1028,11 @@ function setupUserRequestDeletedSubscription() {
return userRequestDeletedSub return userRequestDeletedSub
} }
import { importToPersonalWorkspace } from "./import"
export const def: CollectionsPlatformDef = { export const def: CollectionsPlatformDef = {
initCollectionsSync, initCollectionsSync,
importToPersonalWorkspace,
} }
function getCollectionPathFromCollectionID( function getCollectionPathFromCollectionID(

View file

@ -1,8 +1,3 @@
import {
runGQLQuery,
runGQLSubscription,
runMutation,
} from "@helpers/backend/GQLClient"
import { import {
CreateGqlChildUserCollectionDocument, CreateGqlChildUserCollectionDocument,
CreateGqlChildUserCollectionMutation, CreateGqlChildUserCollectionMutation,
@ -34,12 +29,6 @@ import {
ExportUserCollectionsToJsonDocument, ExportUserCollectionsToJsonDocument,
ExportUserCollectionsToJsonQuery, ExportUserCollectionsToJsonQuery,
ExportUserCollectionsToJsonQueryVariables, ExportUserCollectionsToJsonQueryVariables,
GetGqlRootUserCollectionsDocument,
GetGqlRootUserCollectionsQuery,
GetGqlRootUserCollectionsQueryVariables,
GetUserRootCollectionsDocument,
GetUserRootCollectionsQuery,
GetUserRootCollectionsQueryVariables,
ImportUserCollectionsFromJsonDocument, ImportUserCollectionsFromJsonDocument,
ImportUserCollectionsFromJsonMutation, ImportUserCollectionsFromJsonMutation,
ImportUserCollectionsFromJsonMutationVariables, ImportUserCollectionsFromJsonMutationVariables,
@ -82,6 +71,11 @@ import {
UserRequestUpdatedDocument, UserRequestUpdatedDocument,
UserRootCollectionsSortedDocument, UserRootCollectionsSortedDocument,
} from "@app/api/generated/graphql" } from "@app/api/generated/graphql"
import {
runGQLQuery,
runGQLSubscription,
runMutation,
} from "@hoppscotch/common/helpers/backend/GQLClient"
export const createRESTRootUserCollection = (title: string, data?: string) => export const createRESTRootUserCollection = (title: string, data?: string) =>
runMutation< runMutation<
@ -300,26 +294,6 @@ export const updateUserCollectionOrder = (
nextCollectionID, nextCollectionID,
})() })()
export const getUserRootCollections = () =>
runGQLQuery<
GetUserRootCollectionsQuery,
GetUserRootCollectionsQueryVariables,
""
>({
query: GetUserRootCollectionsDocument,
variables: {},
})
export const getGQLRootUserCollections = () =>
runGQLQuery<
GetGqlRootUserCollectionsQuery,
GetGqlRootUserCollectionsQueryVariables,
""
>({
query: GetGqlRootUserCollectionsDocument,
variables: {},
})
export const exportUserCollectionsToJSON = ( export const exportUserCollectionsToJSON = (
collectionID?: string, collectionID?: string,
collectionType: ReqType.Rest | ReqType.Gql = ReqType.Rest collectionType: ReqType.Rest | ReqType.Gql = ReqType.Rest

View file

@ -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
)
)
})
}

View file

@ -67,6 +67,7 @@ import {
} from "@hoppscotch/data" } from "@hoppscotch/data"
import * as E from "fp-ts/Either" import * as E from "fp-ts/Either"
import { gqlCollectionsSyncer } from "./gqlCollections.sync" import { gqlCollectionsSyncer } from "./gqlCollections.sync"
import { importToPersonalWorkspace } from "./import"
function initCollectionsSync() { function initCollectionsSync() {
const currentUser$ = platformAuth.getCurrentUserStream() const currentUser$ = platformAuth.getCurrentUserStream()
@ -98,7 +99,7 @@ function initCollectionsSync() {
}) })
} }
type ExportedUserCollectionREST = { export type ExportedUserCollectionREST = {
id?: string id?: string
_ref_id?: string _ref_id?: string
folders: ExportedUserCollectionREST[] folders: ExportedUserCollectionREST[]
@ -107,7 +108,7 @@ type ExportedUserCollectionREST = {
data: string data: string
} }
type ExportedUserCollectionGQL = { export type ExportedUserCollectionGQL = {
id?: string id?: string
_ref_id?: string _ref_id?: string
folders: ExportedUserCollectionGQL[] folders: ExportedUserCollectionGQL[]
@ -125,7 +126,7 @@ function addDescriptionField(
})) }))
} }
function exportedCollectionToHoppCollection( export function exportedCollectionToHoppCollection(
collection: ExportedUserCollectionREST | ExportedUserCollectionGQL, collection: ExportedUserCollectionREST | ExportedUserCollectionGQL,
collectionType: "REST" | "GQL" collectionType: "REST" | "GQL"
): HoppCollection { ): HoppCollection {
@ -1031,6 +1032,7 @@ function setupUserRequestDeletedSubscription() {
export const def: CollectionsPlatformDef = { export const def: CollectionsPlatformDef = {
initCollectionsSync, initCollectionsSync,
importToPersonalWorkspace,
} }
function getCollectionPathFromCollectionID( function getCollectionPathFromCollectionID(