diff --git a/packages/hoppscotch-app/helpers/functional/object.ts b/packages/hoppscotch-app/helpers/functional/object.ts index 7058ff053..37ab33996 100644 --- a/packages/hoppscotch-app/helpers/functional/object.ts +++ b/packages/hoppscotch-app/helpers/functional/object.ts @@ -1,6 +1,7 @@ import { pipe } from "fp-ts/function" import cloneDeep from "lodash/cloneDeep" import isEqual from "lodash/isEqual" +import { JSPrimitive, TypeFromPrimitive } from "./primtive" export const objRemoveKey = (key: K) => @@ -19,34 +20,15 @@ export const objFieldMatches = (obj: T): obj is T & { [_ in K]: V } => matches.findIndex((x) => isEqual(obj[fieldName], x)) !== -1 -type JSPrimitive = - | "undefined" - | "object" - | "boolean" - | "number" - | "bigint" - | "string" - | "symbol" - | "function" - -type TypeFromPrimitive

= - P extends "undefined" - ? undefined - : P extends "object" - ? object | null // typeof null === "object" - : P extends "boolean" - ? boolean - : P extends "number" - ? number - : P extends "bigint" - ? BigInt - : P extends "string" - ? string - : P extends "symbol" - ? Symbol - : P extends "function" - ? Function - : unknown +export const objHasProperty = + ( + prop: K, + type?: P + ) => + // eslint-disable-next-line + (obj: O): obj is O & { [_ in K]: TypeFromPrimitive

} => + // eslint-disable-next-line + prop in obj && (type === undefined || typeof (obj as any)[prop] === type) type TypeFromPrimitiveArray

= P extends "undefined" @@ -67,16 +49,6 @@ type TypeFromPrimitiveArray

= ? Function[] : unknown[] -export const objHasProperty = - ( - prop: K, - type?: P - ) => - // eslint-disable-next-line - (obj: O): obj is O & { [_ in K]: TypeFromPrimitive

} => - // eslint-disable-next-line - prop in obj && (type === undefined || typeof (obj as any)[prop] === type) - export const objHasArrayProperty = ( prop: K, diff --git a/packages/hoppscotch-app/helpers/functional/primtive.ts b/packages/hoppscotch-app/helpers/functional/primtive.ts new file mode 100644 index 000000000..3b55fc900 --- /dev/null +++ b/packages/hoppscotch-app/helpers/functional/primtive.ts @@ -0,0 +1,34 @@ +export type JSPrimitive = + | "undefined" + | "object" + | "boolean" + | "number" + | "bigint" + | "string" + | "symbol" + | "function" + +export type TypeFromPrimitive

= + P extends "undefined" + ? undefined + : P extends "object" + ? object | null // typeof null === "object" + : P extends "boolean" + ? boolean + : P extends "number" + ? number + : P extends "bigint" + ? BigInt + : P extends "string" + ? string + : P extends "symbol" + ? Symbol + : P extends "function" + ? Function + : unknown + +export const isOfType = + (type: T) => + (value: unknown): value is T => + // eslint-disable-next-line valid-typeof + typeof value === type diff --git a/packages/hoppscotch-app/helpers/functional/taskEither.ts b/packages/hoppscotch-app/helpers/functional/taskEither.ts new file mode 100644 index 000000000..bd88d65b7 --- /dev/null +++ b/packages/hoppscotch-app/helpers/functional/taskEither.ts @@ -0,0 +1,13 @@ +import * as TE from "fp-ts/TaskEither" + +/** + * A utility type which gives you the type of the left value of a TaskEither + */ +export type TELeftType> = + T extends TE.TaskEither< + infer U, + // eslint-disable-next-line + infer _ + > + ? U + : never diff --git a/packages/hoppscotch-app/helpers/import-export/import/gist.ts b/packages/hoppscotch-app/helpers/import-export/import/gist.ts index 709ce45fa..871906801 100644 --- a/packages/hoppscotch-app/helpers/import-export/import/gist.ts +++ b/packages/hoppscotch-app/helpers/import-export/import/gist.ts @@ -10,7 +10,7 @@ import { defineImporter, IMPORTER_INVALID_FILE_FORMAT } from "." // TODO: Add validation to output const fetchGist = ( url: string -): TO.TaskOption> => +): TO.TaskOption[]> => pipe( TO.tryCatch(() => axios.get(`https://api.github.com/gists/${url.split("/").pop()}`, { @@ -30,8 +30,10 @@ const fetchGist = ( ) export default defineImporter({ + id: "gist", name: "import.from_gist", icon: "github", + applicableTo: ["my-collections", "team-collections"], steps: [ step({ stepName: "URL_IMPORT", diff --git a/packages/hoppscotch-app/helpers/import-export/import/hopp.ts b/packages/hoppscotch-app/helpers/import-export/import/hopp.ts index 29f741770..d75069e18 100644 --- a/packages/hoppscotch-app/helpers/import-export/import/hopp.ts +++ b/packages/hoppscotch-app/helpers/import-export/import/hopp.ts @@ -1,13 +1,22 @@ -import { pipe } from "fp-ts/function" +import { pipe, flow } from "fp-ts/function" import * as TE from "fp-ts/TaskEither" -import * as E from "fp-ts/Either" -import { translateToNewRESTCollection } from "@hoppscotch/data" +import * as O from "fp-ts/Option" +import * as RA from "fp-ts/ReadonlyArray" +import { + translateToNewRESTCollection, + HoppCollection, + HoppRESTRequest, +} from "@hoppscotch/data" +import _isPlainObject from "lodash/isPlainObject" import { step } from "../steps" import { defineImporter, IMPORTER_INVALID_FILE_FORMAT } from "." +import { safeParseJSON } from "~/helpers/functional/json" export default defineImporter({ + id: "hoppscotch", name: "import.from_json", icon: "folder-plus", + applicableTo: ["my-collections", "team-collections", "url-import"], steps: [ step({ stepName: "FILE_IMPORT", @@ -19,16 +28,40 @@ export default defineImporter({ ] as const, importer: ([content]) => pipe( - E.tryCatch( - () => { - const x = JSON.parse(content) - - return Array.isArray(x) - ? x.map((coll: any) => translateToNewRESTCollection(coll)) - : [translateToNewRESTCollection(x)] - }, - () => IMPORTER_INVALID_FILE_FORMAT + safeParseJSON(content), + O.chain( + flow( + makeCollectionsArray, + RA.map( + flow( + O.fromPredicate(isValidCollection), + O.map(translateToNewRESTCollection) + ) + ), + O.sequenceArray, + O.map(RA.toArray) + ) ), - TE.fromEither + TE.fromOption(() => IMPORTER_INVALID_FILE_FORMAT) ), }) + +/** + * checks if a value is a plain object + */ +const isPlainObject = (value: any): value is object => _isPlainObject(value) + +/** + * checks if a collection matches the schema for a hoppscotch collection. + * as of now we are only checking if the collection has a "v" key in it. + */ +const isValidCollection = ( + collection: unknown +): collection is HoppCollection => + isPlainObject(collection) && "v" in collection + +/** + * convert single collection object into an array so it can be handled the same as multiple collections + */ +const makeCollectionsArray = (collections: unknown | unknown[]): unknown[] => + Array.isArray(collections) ? collections : [collections] diff --git a/packages/hoppscotch-app/helpers/import-export/import/importers.ts b/packages/hoppscotch-app/helpers/import-export/import/importers.ts index 559a247d2..de7a22f02 100644 --- a/packages/hoppscotch-app/helpers/import-export/import/importers.ts +++ b/packages/hoppscotch-app/helpers/import-export/import/importers.ts @@ -13,3 +13,10 @@ export const RESTCollectionImporters = [ GistImporter, MyCollectionsImporter, ] as const + +export const URLImporters = [ + HoppRESTCollImporter, + OpenAPIImporter, + PostmanImporter, + InsomniaImporter, +] as const diff --git a/packages/hoppscotch-app/helpers/import-export/import/index.ts b/packages/hoppscotch-app/helpers/import-export/import/index.ts index f734819c8..d2772676f 100644 --- a/packages/hoppscotch-app/helpers/import-export/import/index.ts +++ b/packages/hoppscotch-app/helpers/import-export/import/index.ts @@ -13,10 +13,18 @@ type HoppImporter = ( stepValues: StepsOutputList ) => TE.TaskEither +type HoppImporterApplicableTo = Array< + "team-collections" | "my-collections" | "url-import" +> + /** * Definition for importers */ type HoppImporterDefinition = { + /** + * the id + */ + id: string /** * Name of the importer, shown on the Select Importer dropdown */ @@ -30,7 +38,7 @@ type HoppImporterDefinition = { /** * Identifier for the importer */ - applicableTo?: Array<"team-collections" | "my-collections"> + applicableTo: HoppImporterApplicableTo /** * The importer function, It is a Promise because its supposed to be loaded in lazily (dynamic imports ?) @@ -47,10 +55,11 @@ type HoppImporterDefinition = { * Defines a Hoppscotch importer */ export const defineImporter = (input: { + id: string name: string icon: string importer: HoppImporter - applicableTo?: Array<"team-collections" | "my-collections"> + applicableTo: HoppImporterApplicableTo steps: StepType }) => { return >{ diff --git a/packages/hoppscotch-app/helpers/import-export/import/insomnia.ts b/packages/hoppscotch-app/helpers/import-export/import/insomnia.ts index a5280e4e5..a8c8f1ad6 100644 --- a/packages/hoppscotch-app/helpers/import-export/import/insomnia.ts +++ b/packages/hoppscotch-app/helpers/import-export/import/insomnia.ts @@ -210,7 +210,9 @@ const getHoppCollections = (doc: InsomniaDoc) => ) export default defineImporter({ + id: "insomnia", name: "import.from_insomnia", + applicableTo: ["my-collections", "team-collections", "url-import"], icon: "insomnia", steps: [ step({ diff --git a/packages/hoppscotch-app/helpers/import-export/import/myCollections.ts b/packages/hoppscotch-app/helpers/import-export/import/myCollections.ts index 0c8942d31..da5238d0e 100644 --- a/packages/hoppscotch-app/helpers/import-export/import/myCollections.ts +++ b/packages/hoppscotch-app/helpers/import-export/import/myCollections.ts @@ -6,6 +6,7 @@ import { defineImporter } from "." import { getRESTCollection } from "~/newstore/collections" export default defineImporter({ + id: "myCollections", name: "import.from_my_collections", icon: "user", applicableTo: ["team-collections"], diff --git a/packages/hoppscotch-app/helpers/import-export/import/openapi.ts b/packages/hoppscotch-app/helpers/import-export/import/openapi.ts index 2d25f09ba..cdef55b21 100644 --- a/packages/hoppscotch-app/helpers/import-export/import/openapi.ts +++ b/packages/hoppscotch-app/helpers/import-export/import/openapi.ts @@ -27,7 +27,7 @@ import * as RA from "fp-ts/ReadonlyArray" import { step } from "../steps" import { defineImporter, IMPORTER_INVALID_FILE_FORMAT } from "." -const OPENAPI_DEREF_ERROR = "openapi/deref_error" as const +export const OPENAPI_DEREF_ERROR = "openapi/deref_error" as const // TODO: URL Import Support @@ -586,7 +586,9 @@ const parseOpenAPIDocContent = (str: string) => ) export default defineImporter({ + id: "openapi", name: "import.from_openapi", + applicableTo: ["my-collections", "team-collections", "url-import"], icon: "file", steps: [ step({ @@ -603,7 +605,6 @@ export default defineImporter({ fileContent, parseOpenAPIDocContent, TE.fromOption(() => IMPORTER_INVALID_FILE_FORMAT), - // Try validating, else the importer is invalid file format TE.chainW((obj) => pipe( @@ -613,7 +614,6 @@ export default defineImporter({ ) ) ), - // Deference the references TE.chainW((obj) => pipe( @@ -623,7 +623,6 @@ export default defineImporter({ ) ) ), - TE.chainW(convertOpenApiDocToHopp) ), }) diff --git a/packages/hoppscotch-app/helpers/import-export/import/postman.ts b/packages/hoppscotch-app/helpers/import-export/import/postman.ts index 058637734..c91259dd6 100644 --- a/packages/hoppscotch-app/helpers/import-export/import/postman.ts +++ b/packages/hoppscotch-app/helpers/import-export/import/postman.ts @@ -297,7 +297,9 @@ const getHoppFolder = (ig: ItemGroup): HoppCollection => export const getHoppCollection = (coll: PMCollection) => getHoppFolder(coll) export default defineImporter({ + id: "postman", name: "import.from_postman", + applicableTo: ["my-collections", "team-collections", "url-import"], icon: "postman", steps: [ step({ diff --git a/packages/hoppscotch-app/locales/en.json b/packages/hoppscotch-app/locales/en.json index 4f11734c9..b7d058558 100644 --- a/packages/hoppscotch-app/locales/en.json +++ b/packages/hoppscotch-app/locales/en.json @@ -266,7 +266,11 @@ "from_url": "Import from URL", "gist_url": "Enter Gist URL", "json_description": "Import collections from a Hoppscotch Collections JSON file", - "title": "Import" + "title": "Import", + "import_from_url_success": "Collections Imported", + "import_from_url_invalid_file_format": "Error while importing collections", + "import_from_url_invalid_type": "Unsupported type. accepted values are 'hoppscotch', 'openapi', 'postman', 'insomnia'", + "import_from_url_invalid_fetch": "Couldn't get data from the url" }, "layout": { "collapse_collection": "Collapse or Expand Collections", diff --git a/packages/hoppscotch-app/pages/import.vue b/packages/hoppscotch-app/pages/import.vue new file mode 100644 index 000000000..9834ebb57 --- /dev/null +++ b/packages/hoppscotch-app/pages/import.vue @@ -0,0 +1,102 @@ + + +