From 5f4c19da35e90ffa2f3ae27e9b309ee48f9d4e16 Mon Sep 17 00:00:00 2001 From: Dimitri Mitropoulos Date: Wed, 12 May 2021 02:35:00 -0400 Subject: [PATCH] [TypeScript] Phase 1 & 2 (#3370) Co-authored-by: Opender Singh --- .editorconfig | 1 + .eslintignore | 5 +- .eslintrc.js | 101 + .eslintrc.json | 52 - .github/workflows/test.yml | 2 + .gitignore | 2 + .prettierignore | 7 - .prettierrc | 9 - jest.config.js | 20 + package-lock.json | 5309 +++-- package.json | 34 +- packages/insomnia-app/.babelrc | 38 - packages/insomnia-app/.eslintignore | 7 + packages/insomnia-app/.eslintrc.js | 12 + packages/insomnia-app/.flowconfig | 20 - packages/insomnia-app/.gitignore | 3 +- packages/insomnia-app/.storybook/config.js | 6 +- .../insomnia-app/.storybook/webpack.config.js | 3 +- .../{before-each.js => before-each.ts} | 12 +- ...te-for-test.js => redux-state-for-test.ts} | 8 +- ...{setup-after-env.js => setup-after-env.ts} | 1 + packages/insomnia-app/app/__jest__/setup.js | 30 - packages/insomnia-app/app/__jest__/setup.ts | 49 + .../@grpc/{grpc-js.js => grpc-js.ts} | 3 + packages/insomnia-app/app/__mocks__/dummy.js | 1 - packages/insomnia-app/app/__mocks__/dummy.ts | 2 + .../__mocks__/{electron.js => electron.ts} | 37 +- .../app/__mocks__/font-scanner.js | 1 - .../app/__mocks__/font-scanner.ts | 2 + .../{isomorphic-git.js => isomorphic-git.ts} | 5 + .../{node-forge.js => node-forge.ts} | 25 +- .../{node-libcurl.js => node-libcurl.ts} | 149 +- .../{install.test.js => install.test.ts} | 26 +- .../{package.test.js => package.test.ts} | 1 + .../{renderer.test.js => renderer.test.ts} | 1 - .../{crypt.test.js => crypt.test.ts} | 9 - .../app/account/{crypt.js => crypt.ts} | 96 +- .../app/account/{fetch.js => fetch.ts} | 16 +- .../app/account/{session.js => session.ts} | 53 +- .../{nestedfolders.js => nestedfolders.ts} | 12 +- .../__mocks__/{analytics.js => analytics.ts} | 1 + .../app/common/__mocks__/render.js | 6 - .../app/common/__mocks__/render.ts | 6 + .../{analytics.test.js => analytics.test.ts} | 9 +- .../{api-specs.test.js => api-specs.test.ts} | 23 +- .../{constants.test.js => constants.test.ts} | 0 .../{database.test.js => database.test.ts} | 430 +- ...{grpc-paths.test.js => grpc-paths.test.ts} | 30 +- .../__tests__/{har.test.js => har.test.ts} | 234 +- .../{import.test.js => import.test.ts} | 247 +- ...-storage.test.js => local-storage.test.ts} | 32 +- .../__tests__/{misc.test.js => misc.test.ts} | 171 +- .../{render.test.js => render.test.ts} | 147 +- .../app/common/__tests__/sorting.test.js | 360 - .../app/common/__tests__/sorting.test.ts | 1018 + .../{strings.test.js => strings.test.ts} | 1 - .../app/common/{analytics.js => analytics.ts} | 205 +- .../app/common/{api-specs.js => api-specs.ts} | 18 +- .../app/common/{constants.js => constants.ts} | 194 +- packages/insomnia-app/app/common/database.js | 812 - packages/insomnia-app/app/common/database.ts | 830 + .../{documentation.js => documentation.ts} | 12 +- .../app/common/electron-helpers.ts | 50 + ...kspace-label.js => get-workspace-label.ts} | 1 - .../common/{grpc-events.js => grpc-events.ts} | 12 +- .../common/{grpc-paths.js => grpc-paths.ts} | 37 +- .../app/common/{har.js => har.ts} | 420 +- ...otkeys-listener.js => hotkeys-listener.ts} | 43 +- .../app/common/{hotkeys.js => hotkeys.ts} | 138 +- .../app/common/{import.js => import.ts} | 197 +- .../{keyboard-keys.js => keyboard-keys.ts} | 4 +- .../app/common/{log.js => log.ts} | 5 +- ...arkdown-to-html.js => markdown-to-html.ts} | 1 + ...m-designer.js => migrate-from-designer.ts} | 98 +- .../app/common/{misc.js => misc.ts} | 208 +- .../app/common/{render.js => render.ts} | 117 +- ...-or-folder.js => select-file-or-folder.ts} | 50 +- .../{send-request.js => send-request.ts} | 26 +- .../app/common/{sorting.js => sorting.ts} | 8 +- .../app/common/{strings.js => strings.ts} | 1 - ...ess-token-urls.js => access-token-urls.ts} | 0 ...rization-urls.js => authorization-urls.ts} | 0 .../app/datasets/{charsets.js => charsets.ts} | 0 .../{content-types.js => content-types.ts} | 0 .../datasets/{encodings.js => encodings.ts} | 0 .../{header-names.js => header-names.ts} | 0 packages/insomnia-app/app/global.d.ts | 26 + ...ain.development.js => main.development.ts} | 37 +- ...ipc-main.test.js => grpc-ipc-main.test.ts} | 17 +- .../{error-handling.js => error-handling.ts} | 3 +- .../{grpc-ipc-main.js => grpc-ipc-main.ts} | 5 +- .../{local-storage.js => local-storage.ts} | 15 +- ...quirrel-startup.js => squirrel-startup.ts} | 6 +- .../app/main/{updates.js => updates.ts} | 53 +- .../main/{window-utils.js => window-utils.ts} | 241 +- .../app/models/__mocks__/{uuid.js => uuid.ts} | 2 + ...meta.test.js => grpc-request-meta.test.ts} | 22 +- ...c-request.test.js => grpc-request.test.ts} | 17 +- .../{index.test.js => index.test.ts} | 4 +- ...{proto-file.test.js => proto-file.test.ts} | 12 +- ...uest-meta.test.js => request-meta.test.ts} | 12 +- .../{request.test.js => request.test.ts} | 213 +- .../{response.test.js => response.test.ts} | 47 +- .../{workspace.test.js => workspace.test.ts} | 45 +- .../app/models/{api-spec.js => api-spec.ts} | 45 +- .../app/models/client-certificate.js | 72 - .../app/models/client-certificate.ts | 74 + .../models/{cookie-jar.js => cookie-jar.ts} | 74 +- .../models/{environment.js => environment.ts} | 83 +- .../insomnia-app/app/models/git-repository.js | 64 - .../insomnia-app/app/models/git-repository.ts | 66 + ...c-request-meta.js => grpc-request-meta.ts} | 48 +- .../insomnia-app/app/models/grpc-request.js | 102 - .../insomnia-app/app/models/grpc-request.ts | 111 + ...ame.test.js => get-workspace-name.test.ts} | 2 - ...t.js => git-repository-operations.test.ts} | 17 +- .../{is-model.test.js => is-model.test.ts} | 74 +- ...st.js => query-all-workspace-urls.test.ts} | 27 +- ...orkspace-name.js => get-workspace-name.ts} | 4 +- ...ations.js => git-repository-operations.ts} | 7 +- .../app/models/helpers/is-model.js | 46 - .../app/models/helpers/is-model.ts | 55 + ...ce-urls.js => query-all-workspace-urls.ts} | 19 +- ...st-operations.js => request-operations.ts} | 14 +- ...-operations.js => workspace-operations.ts} | 25 +- .../app/models/{index.js => index.ts} | 37 +- .../insomnia-app/app/models/o-auth-2-token.js | 84 - .../insomnia-app/app/models/o-auth-2-token.ts | 88 + .../insomnia-app/app/models/plugin-data.js | 58 - .../insomnia-app/app/models/plugin-data.ts | 69 + .../app/models/proto-directory.js | 59 - .../app/models/proto-directory.ts | 69 + .../insomnia-app/app/models/proto-file.js | 64 - .../insomnia-app/app/models/proto-file.ts | 74 + .../app/models/request-group-meta.js | 50 - .../app/models/request-group-meta.ts | 49 + .../insomnia-app/app/models/request-group.js | 86 - .../insomnia-app/app/models/request-group.ts | 93 + .../{request-meta.js => request-meta.ts} | 57 +- ...{request-version.js => request-version.ts} | 43 +- .../app/models/{request.js => request.ts} | 215 +- .../app/models/{response.js => response.ts} | 195 +- .../app/models/{settings.js => settings.ts} | 150 +- .../app/models/{stats.js => stats.ts} | 76 +- ...nit-test-result.js => unit-test-result.ts} | 23 +- .../app/models/unit-test-suite.js | 52 - .../app/models/unit-test-suite.ts | 51 + packages/insomnia-app/app/models/unit-test.js | 53 - packages/insomnia-app/app/models/unit-test.ts | 55 + .../insomnia-app/app/models/workspace-meta.js | 110 - .../insomnia-app/app/models/workspace-meta.ts | 110 + .../app/models/{workspace.js => workspace.ts} | 68 +- ...ication.test.js => authentication.test.ts} | 39 +- ....test.js => certificate-url-parse.test.ts} | 10 +- .../{multipart.test.js => multipart.test.ts} | 66 +- .../{network.test.js => network.test.ts} | 304 +- ....test.js => url-matches-cert-host.test.ts} | 3 + .../{authentication.js => authentication.ts} | 35 +- .../{axios-request.js => axios-request.ts} | 17 +- ...{get-header.test.js => get-header.test.ts} | 1 + .../{get-header.js => get-header.ts} | 17 +- .../{get-header.js => get-header.ts} | 9 +- packages/insomnia-app/app/network/ca-certs.js | 3 +- ...-url-parse.js => certificate-url-parse.ts} | 5 +- .../app/network/grpc/__mocks__/index.js | 7 - .../app/network/grpc/__mocks__/index.ts | 7 + ...a.js => grpc-ipc-message-params-schema.ts} | 5 +- ...a.js => grpc-ipc-request-params-schema.ts} | 2 +- ...{call-cache.test.js => call-cache.test.ts} | 45 +- .../{index.test.js => index.test.ts} | 274 +- .../{method.test.js => method.test.ts} | 38 +- ...rpc-url.test.js => parse-grpc-url.test.ts} | 0 .../{prepare.test.js => prepare.test.ts} | 47 +- ...tion.test.js => proto-integration.test.ts} | 28 +- ...cks.test.js => response-callbacks.test.ts} | 18 +- .../grpc/{call-cache.js => call-cache.ts} | 21 +- .../app/network/grpc/{index.js => index.ts} | 60 +- .../app/network/grpc/{method.js => method.ts} | 26 +- .../app/network/grpc/parse-grpc-url.js | 17 - .../app/network/grpc/parse-grpc-url.ts | 34 + .../network/grpc/{prepare.js => prepare.ts} | 31 +- .../__mocks__/{index.js => index.ts} | 0 .../{index.test.js => index.test.ts} | 19 +- ...-file.test.js => write-proto-file.test.ts} | 55 +- .../grpc/proto-loader/{index.js => index.ts} | 26 +- ...rite-proto-file.js => write-proto-file.ts} | 40 +- .../{index.test.js => index.test.ts} | 126 +- ...test.js => ingest-proto-directory.test.ts} | 29 +- .../proto-manager/{index.js => index.tsx} | 49 +- ...directory.js => ingest-proto-directory.ts} | 46 +- ...nse-callbacks.js => response-callbacks.ts} | 12 +- .../{service-error.js => service-error.ts} | 16 +- .../network/{multipart.js => multipart.ts} | 26 +- .../app/network/{network.js => network.ts} | 283 +- .../o-auth-1/{constants.js => constants.ts} | 1 - .../o-auth-1/{get-token.js => get-token.ts} | 46 +- ...st.js => grant-authorization-code.test.ts} | 115 +- ...st.js => grant-client-credentials.test.ts} | 77 +- ...mplicit.test.js => grant-implicit.test.ts} | 4 +- ...assword.test.js => grant-password.test.ts} | 87 +- .../__tests__/{helpers.js => helpers.ts} | 5 + .../__tests__/{misc.test.js => misc.test.ts} | 9 - .../o-auth-2/{constants.js => constants.ts} | 3 - .../o-auth-2/{get-token.js => get-token.ts} | 23 +- ...on-code.js => grant-authorization-code.ts} | 168 +- ...entials.js => grant-client-credentials.ts} | 60 +- .../{grant-implicit.js => grant-implicit.ts} | 55 +- .../{grant-password.js => grant-password.ts} | 60 +- .../app/network/o-auth-2/{misc.js => misc.ts} | 21 +- .../{refresh-token.js => refresh-token.ts} | 40 +- ...-cert-host.js => url-matches-cert-host.ts} | 4 +- .../__tests__/{app.test.js => app.test.ts} | 28 +- .../__tests__/{data.test.js => data.test.ts} | 23 +- .../{request.test.js => request.test.ts} | 106 +- .../{response.test.js => response.test.ts} | 34 +- .../{store.test.js => store.test.ts} | 27 +- .../app/plugins/context/{app.js => app.tsx} | 66 +- .../app/plugins/context/{data.js => data.ts} | 39 +- .../plugins/context/{index.js => index.ts} | 1 - .../app/plugins/context/network.js | 18 - .../app/plugins/context/network.ts | 16 + .../context/{request.js => request.ts} | 148 +- .../context/{response.js => response.ts} | 57 +- .../insomnia-app/app/plugins/context/store.js | 43 - .../insomnia-app/app/plugins/context/store.ts | 57 + .../app/plugins/{create.js => create.ts} | 5 +- .../app/plugins/{index.js => index.ts} | 286 +- .../app/plugins/{install.js => install.ts} | 68 +- .../app/plugins/{misc.js => misc.ts} | 136 +- packages/insomnia-app/app/renderer.html | 9 +- .../app/{renderer.js => renderer.ts} | 0 .../__tests__/{diff.test.js => diff.test.ts} | 51 +- .../{patch.test.js => patch.test.ts} | 2 +- .../app/sync/delta/{diff.js => diff.ts} | 42 +- .../app/sync/delta/{patch.js => patch.ts} | 3 +- .../sync/git/__mocks__/{path.js => path.ts} | 5 +- .../app/sync/git/__mocks__/shallow-clone.js | 3 - .../app/sync/git/__mocks__/shallow-clone.ts | 4 + ...-rollback.test.js => git-rollback.test.ts} | 102 +- .../{git-vcs.test.js => git-vcs.test.ts} | 86 +- ...{mem-client.test.js => mem-client.test.ts} | 87 +- ...db-client.test.js => ne-db-client.test.ts} | 101 +- ...it-path.test.js => parse-git-path.test.ts} | 11 - .../{path-sep.test.js => path-sep.test.ts} | 4 +- ...ent.test.js => routable-fs-client.test.ts} | 11 +- .../sync/git/__tests__/{util.js => util.ts} | 2 +- .../{utils.test.js => utils.test.ts} | 1 - .../sync/git/{fs-client.js => fs-client.ts} | 24 +- .../git/{git-rollback.js => git-rollback.ts} | 15 +- .../app/sync/git/{git-vcs.js => git-vcs.ts} | 211 +- .../git/{http-client.js => http-client.ts} | 2 +- .../sync/git/{mem-client.js => mem-client.ts} | 226 +- .../git/{ne-db-client.js => ne-db-client.ts} | 78 +- .../{parse-git-path.js => parse-git-path.ts} | 17 +- .../app/sync/git/{path-sep.js => path-sep.ts} | 1 - ...ble-fs-client.js => routable-fs-client.ts} | 15 +- .../{shallow-clone.js => shallow-clone.ts} | 16 +- .../app/sync/git/{stat.js => stat.ts} | 18 +- .../insomnia-app/app/sync/git/system-error.ts | 46 + .../app/sync/git/{utils.js => utils.ts} | 12 +- ...test.js => deterministicStringify.test.ts} | 15 +- ...Stringify.js => deterministicStringify.ts} | 16 +- .../{index.test.js => index.test.ts} | 45 +- .../sync/store/drivers/{base.js => base.ts} | 10 +- ...system-driver.js => file-system-driver.ts} | 41 +- .../{memory-driver.js => memory-driver.ts} | 21 +- .../{compress.test.js => compress.test.ts} | 0 .../store/hooks/{compress.js => compress.ts} | 5 +- .../app/sync/store/{index.js => index.ts} | 27 +- packages/insomnia-app/app/sync/types.js | 107 - packages/insomnia-app/app/sync/types.ts | 104 + .../{index.test.js => index.test.ts} | 369 +- .../{paths.test.js => paths.test.ts} | 0 .../__tests__/{util.test.js => util.test.ts} | 343 +- .../app/sync/vcs/{index.js => index.ts} | 452 +- packages/insomnia-app/app/sync/vcs/paths.js | 42 - packages/insomnia-app/app/sync/vcs/paths.ts | 41 + .../app/sync/vcs/{util.js => util.ts} | 145 +- .../{utils.test.js => utils.test.ts} | 224 +- .../{base-extension.js => base-extension.ts} | 61 +- .../app/templating/extensions/index.js | 100 - .../app/templating/extensions/index.ts | 100 + .../app/templating/{index.js => index.ts} | 57 +- .../app/templating/{utils.js => utils.ts} | 160 +- .../app/{test-utils.js => test-utils.ts} | 29 +- ...activity-toggle.js => activity-toggle.tsx} | 37 +- .../{analytics.js => analytics.tsx} | 27 +- .../{editable.test.js => editable.test.ts} | 0 .../components/base/{button.js => button.tsx} | 40 +- .../base/{copy-button.js => copy-button.tsx} | 54 +- ...debounced-input.js => debounced-input.tsx} | 70 +- ...dropdown-button.js => dropdown-button.tsx} | 18 +- ...opdown-divider.js => dropdown-divider.tsx} | 15 +- .../{dropdown-hint.js => dropdown-hint.tsx} | 11 +- .../{dropdown-item.js => dropdown-item.tsx} | 48 +- .../{dropdown-right.js => dropdown-right.tsx} | 15 +- .../dropdown/{dropdown.js => dropdown.tsx} | 183 +- .../base/dropdown/{index.js => index.ts} | 0 ...itable.stories.js => editable.stories.tsx} | 4 +- .../base/{editable.js => editable.tsx} | 69 +- ...-input-button.js => file-input-button.tsx} | 37 +- .../base/{highlight.js => highlight.tsx} | 23 +- ...checkbox.js => indeterminate-checkbox.tsx} | 22 +- .../ui/components/base/{lazy.js => lazy.ts} | 28 +- .../ui/components/base/{link.js => link.tsx} | 44 +- .../components/base/{mailto.js => mailto.tsx} | 31 +- .../base/{modal-body.js => modal-body.tsx} | 18 +- .../{modal-footer.js => modal-footer.tsx} | 18 +- .../{modal-header.js => modal-header.tsx} | 22 +- .../components/base/{modal.js => modal.tsx} | 90 +- ...n.stories.js => prompt-button.stories.tsx} | 4 +- .../{prompt-button.js => prompt-button.tsx} | 105 +- ...tton.test.js => grpc-send-button.test.tsx} | 14 - ...pc-send-button.js => grpc-send-button.tsx} | 21 +- ...settings-button.js => settings-button.tsx} | 25 +- ...button.js => check-for-updates-button.tsx} | 54 +- .../{base-imports.js => base-imports.ts} | 5 +- .../{code-editor.js => code-editor.tsx} | 403 +- .../{autocomplete.js => autocomplete.ts} | 100 +- .../extensions/{clickable.js => clickable.ts} | 15 +- .../{nunjucks-tags.js => nunjucks-tags.ts} | 153 +- ...async-lint.js => javascript-async-lint.ts} | 12 +- .../lint/{openapi.js => openapi.ts} | 3 +- .../codemirror/modes/{curl.js => curl.ts} | 16 +- .../modes/{nunjucks.js => nunjucks.ts} | 15 +- .../modes/{openapi.js => openapi.ts} | 12 +- ...one-line-editor.js => one-line-editor.tsx} | 171 +- .../{cookie-list.js => cookie-list.tsx} | 54 +- ...count-dropdown.js => account-dropdown.tsx} | 19 +- .../{auth-dropdown.js => auth-dropdown.tsx} | 27 +- ...-dropdown.js => content-type-dropdown.tsx} | 32 +- ...dropdown.js => document-card-dropdown.tsx} | 69 +- ...-dropdown.js => environments-dropdown.tsx} | 51 +- ...sync-dropdown.js => git-sync-dropdown.tsx} | 176 +- ...s => grpc-method-dropdown-button.test.tsx} | 5 - ....test.js => grpc-method-dropdown.test.tsx} | 17 - ...ton.js => grpc-method-dropdown-button.tsx} | 13 +- ...d-dropdown.js => grpc-method-dropdown.tsx} | 32 +- .../{index.js => index.ts} | 0 ...method-dropdown.js => method-dropdown.tsx} | 40 +- ...-dropdown.js => preview-mode-dropdown.tsx} | 20 +- ...down.js => remote-workspaces-dropdown.tsx} | 74 +- ...opdown.js => request-actions-dropdown.tsx} | 110 +- ....js => request-group-actions-dropdown.tsx} | 85 +- ...pdown.js => response-history-dropdown.tsx} | 85 +- .../{sync-dropdown.js => sync-dropdown.tsx} | 210 +- ...ace-dropdown.js => workspace-dropdown.tsx} | 101 +- ...tor.test.js => environment-editor.test.ts} | 1 - ...ditor.test.js => password-editor.test.tsx} | 4 +- .../auth/{asap-auth.js => asap-auth.tsx} | 54 +- .../{auth-wrapper.js => auth-wrapper.tsx} | 40 +- .../auth/{aws-auth.js => aws-auth.tsx} | 34 +- .../auth/{basic-auth.js => basic-auth.tsx} | 33 +- .../auth/{bearer-auth.js => bearer-auth.tsx} | 29 +- .../auth/{digest-auth.js => digest-auth.tsx} | 34 +- .../auth/{hawk-auth.js => hawk-auth.tsx} | 81 +- .../auth/{netrc-auth.js => netrc-auth.tsx} | 0 .../auth/{ntlm-auth.js => ntlm-auth.tsx} | 33 +- .../{o-auth-1-auth.js => o-auth-1-auth.tsx} | 120 +- .../{o-auth-2-auth.js => o-auth-2-auth.tsx} | 245 +- .../body/{body-editor.js => body-editor.tsx} | 57 +- .../body/{file-editor.js => file-editor.tsx} | 18 +- .../body/{form-editor.js => form-editor.tsx} | 26 +- ...graph-ql-editor.js => graph-ql-editor.tsx} | 253 +- .../body/{raw-editor.js => raw-editor.tsx} | 50 +- ...coded-editor.js => url-encoded-editor.tsx} | 26 +- ...nment-editor.js => environment-editor.tsx} | 79 +- .../{grpc-editor.js => grpc-editor.tsx} | 27 +- ...password-editor.js => password-editor.tsx} | 44 +- ...s-editor.js => request-headers-editor.tsx} | 47 +- ...ditor.js => request-parameters-editor.tsx} | 42 +- .../{error-boundary.js => error-boundary.tsx} | 69 +- ...est-group-row.js => request-group-row.tsx} | 38 +- .../{request-row.js => request-row.tsx} | 21 +- .../export-requests/{tree.js => tree.tsx} | 22 +- .../{forms.stories.js => forms.stories.tsx} | 4 +- ...lt-value.js => graph-ql-default-value.tsx} | 14 +- ...rer-enum.js => graph-ql-explorer-enum.tsx} | 17 +- ...nk.js => graph-ql-explorer-field-link.tsx} | 18 +- ...r-field.js => graph-ql-explorer-field.tsx} | 28 +- ...schema.js => graph-ql-explorer-schema.tsx} | 28 +- ...ink.js => graph-ql-explorer-type-link.tsx} | 28 +- ...rer-type.js => graph-ql-explorer-type.tsx} | 51 +- ...h-ql-explorer.js => graph-ql-explorer.tsx} | 77 +- .../app/ui/components/grpc-spinner.js | 17 - .../app/ui/components/grpc-spinner.tsx | 15 + .../app/ui/components/help-tooltip.js | 29 - ...ip.stories.js => help-tooltip.stories.tsx} | 4 +- .../app/ui/components/help-tooltip.tsx | 33 + .../ui/components/{hotkey.js => hotkey.tsx} | 33 +- ...nt-wrapper.js => html-element-wrapper.tsx} | 16 +- .../{editor.js => editor.tsx} | 150 +- .../key-value-editor/{row.js => row.tsx} | 288 +- .../{keydown-binder.js => keydown-binder.ts} | 23 +- ...markdown-editor.js => markdown-editor.tsx} | 60 +- ...rkdown-preview.js => markdown-preview.tsx} | 66 +- .../modals/__mocks__/{index.js => index.ts} | 1 + ...modal.js => add-key-combination-modal.tsx} | 43 +- .../{alert-modal.js => alert-modal.tsx} | 53 +- .../modals/{ask-modal.js => ask-modal.tsx} | 57 +- ...-prompt-modal.js => code-prompt-modal.tsx} | 88 +- ...odify-modal.js => cookie-modify-modal.tsx} | 102 +- .../{cookies-modal.js => cookies-modal.tsx} | 76 +- ...it-modal.js => environment-edit-modal.tsx} | 65 +- .../{error-modal.js => error-modal.tsx} | 46 +- ...sts-modal.js => export-requests-modal.tsx} | 80 +- ...er-help-modal.js => filter-help-modal.tsx} | 25 +- ...-code-modal.js => generate-code-modal.tsx} | 90 +- ...fig-modal.js => generate-config-modal.tsx} | 74 +- ...anches-modal.js => git-branches-modal.tsx} | 112 +- .../{git-log-modal.js => git-log-modal.tsx} | 43 +- ...l.js => git-repository-settings-modal.tsx} | 83 +- ...staging-modal.js => git-staging-modal.tsx} | 144 +- .../components/modals/{index.js => index.ts} | 13 +- .../{login-modal.js => login-modal.tsx} | 64 +- ...-modal.js => move-request-group-modal.tsx} | 56 +- .../{nunjucks-modal.js => nunjucks-modal.tsx} | 58 +- ...odal.js => payment-notification-modal.tsx} | 16 +- .../{prompt-modal.js => prompt-modal.tsx} | 148 +- ...o-files-modal.js => proto-files-modal.tsx} | 85 +- ...eate-modal.js => request-create-modal.tsx} | 85 +- ...odal.js => request-render-error-modal.tsx} | 29 +- ...gs-modal.js => request-settings-modal.tsx} | 156 +- ...er-modal.js => request-switcher-modal.tsx} | 170 +- ...ebug-modal.js => response-debug-modal.tsx} | 46 +- .../{select-modal.js => select-modal.tsx} | 68 +- .../{settings-modal.js => settings-modal.tsx} | 91 +- ...nches-modal.js => sync-branches-modal.tsx} | 123 +- ...-delete-modal.js => sync-delete-modal.tsx} | 57 +- ...istory-modal.js => sync-history-modal.tsx} | 52 +- ...nc-merge-modal.js => sync-merge-modal.tsx} | 55 +- ...nc-share-modal.js => sync-share-modal.tsx} | 67 +- ...taging-modal.js => sync-staging-modal.tsx} | 126 +- ... => workspace-environments-edit-modal.tsx} | 195 +- ...-modal.js => workspace-settings-modal.tsx} | 151 +- .../{wrapper-modal.js => wrapper-modal.tsx} | 60 +- .../{notice.stories.js => notice.stories.tsx} | 4 +- .../ui/components/{notice.js => notice.tsx} | 35 +- ...-container.js => onboarding-container.tsx} | 13 +- .../{page-layout.js => page-layout.tsx} | 72 +- .../panes/{blank-pane.js => blank-pane.tsx} | 11 +- ....test.js => use-proto-file-reload.test.ts} | 39 +- .../grpc-request-pane/{index.js => index.tsx} | 43 +- ...ion-handlers.js => use-action-handlers.ts} | 20 +- ...nge-handlers.js => use-change-handlers.ts} | 38 +- ...grpc-urls.js => use-existing-grpc-urls.ts} | 1 - ...ile-reload.js => use-proto-file-reload.ts} | 8 +- ...ected-method.js => use-selected-method.ts} | 33 +- ...esponse-pane.js => grpc-response-pane.tsx} | 19 +- .../ui/components/panes/{pane.js => pane.tsx} | 30 +- ...t-pane.js => placeholder-request-pane.tsx} | 19 +- ...-pane.js => placeholder-response-pane.tsx} | 11 +- .../{request-pane.js => request-pane.tsx} | 107 +- .../{response-pane.js => response-pane.tsx} | 88 +- ...-item.js => proto-directory-list-item.tsx} | 20 +- ...-list-item.js => proto-file-list-item.tsx} | 41 +- ...proto-file-list.js => proto-file-list.tsx} | 26 +- ...{proto-list-item.js => proto-list-item.ts} | 7 +- ...ry-string.js => rendered-query-string.tsx} | 67 +- .../app/ui/components/rendered-text.js | 59 - .../app/ui/components/rendered-text.tsx | 64 + ...request-url-bar.js => request-url-bar.tsx} | 146 +- .../{response-timer.js => response-timer.tsx} | 45 +- .../settings/{account.js => account.tsx} | 91 +- .../settings/{general.js => general.tsx} | 155 +- .../{import-export.js => import-export.tsx} | 28 +- .../settings/{plugins.js => plugins.tsx} | 113 +- .../settings/{shortcuts.js => shortcuts.tsx} | 50 +- .../settings/{theme.js => theme.tsx} | 61 +- ...debar-children.js => sidebar-children.tsx} | 116 +- ...ropdown.js => sidebar-create-dropdown.tsx} | 28 +- .../{sidebar-filter.js => sidebar-filter.tsx} | 39 +- ...p-row.js => sidebar-request-group-row.tsx} | 108 +- ...request-row.js => sidebar-request-row.tsx} | 180 +- .../sidebar/sidebar-sort-dropdown.js | 27 - .../sidebar/sidebar-sort-dropdown.tsx | 22 + .../sidebar/{sidebar.js => sidebar.tsx} | 36 +- ...tor-sidebar.js => spec-editor-sidebar.tsx} | 64 +- ...nc-pull-button.js => sync-pull-button.tsx} | 47 +- ...grpc-method-tag.js => grpc-method-tag.tsx} | 12 +- .../app/ui/components/tags/grpc-status-tag.js | 34 - .../ui/components/tags/grpc-status-tag.tsx | 32 + .../tags/{grpc-tag.js => grpc-tag.tsx} | 5 +- .../tags/{method-tag.js => method-tag.tsx} | 23 +- .../tags/{size-tag.js => size-tag.tsx} | 33 +- .../tags/{status-tag.js => status-tag.tsx} | 36 +- .../tags/{time-tag.js => time-tag.tsx} | 29 +- .../tags/{url-tag.js => url-tag.tsx} | 33 +- .../{tag-editor.js => tag-editor.tsx} | 187 +- ...variable-editor.js => variable-editor.tsx} | 56 +- .../{time-from-now.js => time-from-now.tsx} | 44 +- .../app/ui/components/{toast.js => toast.tsx} | 105 +- ...tooltip.stories.js => tooltip.stories.tsx} | 17 +- .../ui/components/{tooltip.js => tooltip.tsx} | 119 +- .../app/ui/components/unit-test-editable.js | 12 - .../app/ui/components/unit-test-editable.tsx | 13 + ...d-messages.js => grpc-tabbed-messages.tsx} | 51 +- ...-viewer.js => response-cookies-viewer.tsx} | 27 +- ...-csv-viewer.js => response-csv-viewer.tsx} | 37 +- .../{response-error.js => response-error.tsx} | 23 +- ...-viewer.js => response-headers-viewer.tsx} | 32 +- ...se-multipart.js => response-multipart.tsx} | 124 +- ...-pdf-viewer.js => response-pdf-viewer.tsx} | 53 +- .../{response-raw.js => response-raw.tsx} | 18 +- ...viewer.js => response-timeline-viewer.tsx} | 36 +- ...response-viewer.js => response-viewer.tsx} | 114 +- ...onse-web-view.js => response-web-view.tsx} | 36 +- ...ge-header.js => workspace-page-header.tsx} | 32 +- ...per-analytics.js => wrapper-analytics.tsx} | 15 +- .../{wrapper-debug.js => wrapper-debug.tsx} | 94 +- .../{wrapper-design.js => wrapper-design.tsx} | 68 +- .../{wrapper-home.js => wrapper-home.tsx} | 139 +- ...per-migration.js => wrapper-migration.tsx} | 103 +- ...r-onboarding.js => wrapper-onboarding.tsx} | 45 +- ...per-unit-test.js => wrapper-unit-test.tsx} | 171 +- .../ui/components/{wrapper.js => wrapper.tsx} | 383 +- .../app/ui/containers/{app.js => app.tsx} | 617 +- ...g-drop-context.js => drag-drop-context.ts} | 2 +- .../{grpc-actions.js => grpc-actions.ts} | 1 + ...ssage-schema.js => grpc-message-schema.ts} | 2 +- ...ma.js => grpc-method-definition-schema.ts} | 2 +- .../__schemas__/grpc-status-object-schema.js | 9 - .../__schemas__/grpc-status-object-schema.ts | 9 + .../grpc/__schemas__/{index.js => index.ts} | 0 ...tate-schema.js => request-state-schema.ts} | 2 - ...-context.test.js => grpc-context.test.tsx} | 5 - ...erer.test.js => grpc-ipc-renderer.test.ts} | 20 +- ...c-reducer.test.js => grpc-reducer.test.ts} | 153 +- .../grpc/{grpc-actions.js => grpc-actions.ts} | 103 +- .../{grpc-context.js => grpc-context.tsx} | 32 +- ...c-ipc-renderer.js => grpc-ipc-renderer.ts} | 2 - .../grpc/{grpc-reducer.js => grpc-reducer.ts} | 65 +- .../ui/context/grpc/{index.js => index.ts} | 2 - packages/insomnia-app/app/ui/dnd-backend.js | 38 - packages/insomnia-app/app/ui/dnd-backend.ts | 46 + .../app/ui/{index.js => index.tsx} | 16 +- ...ectors.test.js => proto-selectors.test.ts} | 105 +- ...tors.test.js => sidebar-selectors.test.ts} | 42 +- .../app/ui/redux/{create.js => create.ts} | 9 +- .../__tests__/{git.test.js => git.test.tsx} | 267 +- .../{global.test.js => global.test.ts} | 256 +- .../{helpers.test.js => helpers.test.ts} | 6 - .../{workspace.test.js => workspace.test.ts} | 46 +- .../modules/{entities.js => entities.ts} | 24 +- .../app/ui/redux/modules/{git.js => git.tsx} | 56 +- .../redux/modules/{global.js => global.tsx} | 236 +- .../redux/modules/{helpers.js => helpers.ts} | 17 +- .../ui/redux/modules/{index.js => index.ts} | 11 +- .../modules/{workspace.js => workspace.ts} | 28 +- ...{proto-selectors.js => proto-selectors.ts} | 33 +- .../ui/redux/{selectors.js => selectors.ts} | 59 +- ...ebar-selectors.js => sidebar-selectors.ts} | 15 +- packages/insomnia-app/bin/yarn-standalone.js | 2 +- packages/insomnia-app/config/index.js | 10 - packages/insomnia-app/flow-typed/anybase.js | 5 - .../flow-typed/autobind-decorator.js | 5 - packages/insomnia-app/flow-typed/aws4.js | 7 - .../insomnia-app/flow-typed/classnames.js | 5 - packages/insomnia-app/flow-typed/clone.js | 5 - .../insomnia-app/flow-typed/codemirror.js | 35 - packages/insomnia-app/flow-typed/color.js | 15 - .../insomnia-app/flow-typed/deep-equal.js | 5 - .../flow-typed/electron-context-menu.js | 5 - .../flow-typed/electron-squirrel-startup.js | 5 - .../flow-typed/electron-updater.js | 10 - packages/insomnia-app/flow-typed/electron.js | 27 - packages/insomnia-app/flow-typed/events.js | 5 - .../insomnia-app/flow-typed/font-scanner.js | 5 - packages/insomnia-app/flow-typed/fs-extra.js | 7 - packages/insomnia-app/flow-typed/fuzzysort.js | 8 - packages/insomnia-app/flow-typed/hawk.js | 5 - .../insomnia-app/flow-typed/iconv-lite.js | 5 - .../flow-typed/insomnia-cookies.js | 9 - .../flow-typed/insomnia-importers.js | 18 - .../flow-typed/insomnia-plugin-hash.js | 9 - .../flow-typed/insomnia-prettify.js | 7 - .../insomnia-app/flow-typed/insomnia-url.js | 13 - .../insomnia-app/flow-typed/isomorphic-git.js | 5 - packages/insomnia-app/flow-typed/jest.js | 1186 -- .../insomnia-app/flow-typed/json-order.js | 25 - packages/insomnia-app/flow-typed/jsonpath.js | 7 - .../flow-typed/jwt-authentication.js | 5 - .../insomnia-app/flow-typed/mime-types.js | 8 - packages/insomnia-app/flow-typed/mkdirp.js | 7 - packages/insomnia-app/flow-typed/moment.js | 12 - .../insomnia-app/flow-typed/multiparty.js | 5 - packages/insomnia-app/flow-typed/nedb.js | 12 - .../insomnia-app/flow-typed/node-libcurl.js | 62 - packages/insomnia-app/flow-typed/nunjucks.js | 27 - .../insomnia-app/flow-typed/oauth-1.0a.js | 30 - .../insomnia-app/flow-typed/objectpath.js | 5 - .../insomnia-app/flow-typed/openapi-2-kong.js | 9 - packages/insomnia-app/flow-typed/papaparse.js | 7 - .../insomnia-app/flow-typed/pdfjs-dist.js | 5 - packages/insomnia-app/flow-typed/react-dom.js | 11 - .../flow-typed/react-sortable-hoc.js | 11 - .../insomnia-app/flow-typed/react-tabs.js | 12 - packages/insomnia-app/flow-typed/reselect.js | 7 - packages/insomnia-app/flow-typed/rimraf.js | 7 - .../flow-typed/styled-components.js | 432 - .../flow-typed/swagger-ui-react.js | 5 - .../insomnia-app/flow-typed/tough-cookie.js | 5 - packages/insomnia-app/flow-typed/url-join.js | 5 - packages/insomnia-app/flow-typed/uuid.js | 8 - packages/insomnia-app/flow-typed/xmldom.js | 7 - packages/insomnia-app/flow-typed/yaml.js | 9 - packages/insomnia-app/jest.config.js | 35 + packages/insomnia-app/package-lock.json | 1414 +- packages/insomnia-app/package.json | 118 +- .../insomnia-app/scripts/afterSignHook.js | 17 +- packages/insomnia-app/scripts/build.js | 247 - packages/insomnia-app/scripts/build.ts | 247 + ...{getBuildContext.js => getBuildContext.ts} | 50 +- packages/insomnia-app/scripts/package.js | 77 - packages/insomnia-app/scripts/package.ts | 82 + packages/insomnia-app/scripts/release.js | 93 - packages/insomnia-app/scripts/release.ts | 103 + .../scripts/startDevServer.ts} | 0 .../send-request/{index.js => index.ts} | 0 packages/insomnia-app/tsconfig.build.json | 35 + packages/insomnia-app/tsconfig.build.sr.json | 11 + packages/insomnia-app/tsconfig.json | 20 + packages/insomnia-app/tsconfig.webpack.json | 19 + ...g.base.babel.js => webpack.config.base.ts} | 33 +- .../webpack.config.development.babel.js | 55 - .../webpack/webpack.config.development.ts | 58 + ...on.babel.js => webpack.config.electron.ts} | 30 +- ....babel.js => webpack.config.production.ts} | 12 +- .../webpack.config.sr.ts} | 30 +- packages/insomnia-components/.babelrc | 20 - packages/insomnia-components/.eslintignore | 2 + packages/insomnia-components/.eslintrc.js | 7 + packages/insomnia-components/.flowconfig | 16 - packages/insomnia-components/.gitignore | 2 +- .../.storybook/configs/contexts.js | 2 +- .../insomnia-components/.storybook/main.js | 9 + .../insomnia-components/.storybook/manager.js | 58 +- .../insomnia-components/.storybook/preview.js | 2 +- packages/insomnia-components/.svgrrc.json | 8 - .../insomnia-components/__mocks__/dummy.js | 1 - .../components/breadcrumb.stories.js | 8 - .../components/button/async-button.js | 49 - .../components/button/index.js | 3 - .../components/card-container.js | 17 - .../components/header.stories.js | 24 - .../list-group/unit-test-result-badge.js | 39 - .../list-group/unit-test-result-item.js | 53 - .../components/multi-switch.stories.js | 20 - .../components/radio-button-group.stories.js | 23 - .../components/sidebar/sidebar-info.js | 42 - .../sidebar/sidebar-invalid-section.js | 18 - .../components/sidebar/sidebar-panel.js | 24 - .../components/svg-icon.js | 221 - .../components/switch.stories.js | 22 - .../insomnia-components/components/table.js | 123 - .../flow-typed/@storybook/addon-knobs.js | 11 - .../flow-typed/@storybook/framer-motion.js | 10 - .../flow-typed/autobind-decorator.js | 5 - .../flow-typed/class-autobind-decorator.js | 5 - .../insomnia-components/flow-typed/md5.js | 5 - .../flow-typed/npm/classnames_v2.x.x.js | 23 - .../npm/styled-components_v4.x.x.js | 432 - .../flow-typed/react-dom.js | 10 - .../flow-typed/react-switch.js | 7 - .../flow-typed/react-use.js | 7 - packages/insomnia-components/index.js | 48 - packages/insomnia-components/jest.config.js | 21 + .../insomnia-components/package-lock.json | 16130 ++++++++++++---- packages/insomnia-components/package.json | 83 +- .../{ => src}/assets/icn-arrow-right.svg | 0 .../{ => src}/assets/icn-bitbucket-logo.svg | 0 .../{ => src}/assets/icn-brackets.svg | 0 .../{ => src}/assets/icn-burger-menu.svg | 0 .../{ => src}/assets/icn-checkmark.svg | 0 .../{ => src}/assets/icn-chevron-down.svg | 0 .../{ => src}/assets/icn-chevron-up.svg | 0 .../{ => src}/assets/icn-clock.svg | 0 .../{ => src}/assets/icn-cookie.svg | 0 .../{ => src}/assets/icn-drag-grip.svg | 0 .../{ => src}/assets/icn-elevator.svg | 0 .../{ => src}/assets/icn-ellipsis-circle.svg | 0 .../{ => src}/assets/icn-ellipsis.svg | 0 .../{ => src}/assets/icn-empty.svg | 0 .../{ => src}/assets/icn-errors.svg | 0 .../{ => src}/assets/icn-file.svg | 0 .../{ => src}/assets/icn-folder-open.svg | 0 .../{ => src}/assets/icn-folder.svg | 0 .../{ => src}/assets/icn-gear.svg | 0 .../{ => src}/assets/icn-git-branch.svg | 0 .../{ => src}/assets/icn-github-logo.svg | 0 .../{ => src}/assets/icn-gitlab-logo.svg | 0 .../{ => src}/assets/icn-gui.svg | 0 .../{ => src}/assets/icn-indentation.svg | 0 .../{ => src}/assets/icn-info.svg | 0 .../{ => src}/assets/icn-key.svg | 0 .../assets/icn-minus-circle-fill.svg | 0 .../{ => src}/assets/icn-minus-circle.svg | 0 .../{ => src}/assets/icn-placeholder.svg | 0 .../{ => src}/assets/icn-play.svg | 0 .../{ => src}/assets/icn-plus.svg | 0 .../{ => src}/assets/icn-prohibited.svg | 0 .../{ => src}/assets/icn-question-fill.svg | 0 .../{ => src}/assets/icn-question.svg | 0 .../{ => src}/assets/icn-search.svg | 0 .../{ => src}/assets/icn-sec-cert.svg | 0 .../{ => src}/assets/icn-success.svg | 0 .../{ => src}/assets/icn-sync.svg | 0 .../{ => src}/assets/icn-trashcan.svg | 0 .../{ => src}/assets/icn-triangle.svg | 0 .../{ => src}/assets/icn-user.svg | 0 .../{ => src}/assets/icn-warning-circle.svg | 0 .../{ => src}/assets/icn-warning.svg | 0 .../{ => src}/assets/icn-x.svg | 0 .../src/breadcrumb.stories.tsx | 13 + .../breadcrumb.js => src/breadcrumb.tsx} | 30 +- .../button/async-button.stories.tsx} | 19 +- .../src/button/async-button.tsx | 50 + .../button/button.stories.tsx} | 41 +- .../button.js => src/button/button.tsx} | 42 +- .../button/circle-button.stories.tsx} | 10 +- .../button/circle-button.tsx} | 16 +- .../insomnia-components/src/button/index.ts | 3 + .../src/card-container.tsx | 14 + .../card.stories.js => src/card.stories.tsx} | 7 +- .../{components/card.js => src/card.tsx} | 58 +- .../dropdown/dropdown-divider.tsx} | 11 +- .../dropdown/dropdown-item.tsx} | 62 +- .../dropdown/dropdown.stories.tsx} | 15 +- .../dropdown.js => src/dropdown/dropdown.tsx} | 312 +- .../insomnia-components/src/dropdown/index.ts | 3 + .../src/header.stories.tsx | 29 + .../{components/header.js => src/header.tsx} | 21 +- .../help-tooltip.stories.tsx} | 12 +- .../help-tooltip.js => src/help-tooltip.tsx} | 32 +- packages/insomnia-components/src/index.ts | 27 + .../jest/setup-after-env.ts} | 1 + .../src/list-group/index.ts | 7 + .../list-group/list-group-item.tsx} | 13 +- .../list-group/list-group.stories.tsx} | 60 +- .../list-group/list-group.tsx} | 17 +- .../list-group/unit-test-item.tsx} | 77 +- .../unit-test-request-selector.tsx} | 26 +- .../src/list-group/unit-test-result-badge.tsx | 36 + .../src/list-group/unit-test-result-item.tsx | 62 + .../unit-test-result-timestamp.tsx} | 17 +- .../src/multi-switch.stories.tsx | 31 + .../multi-switch.js => src/multi-switch.tsx} | 13 +- .../notice-table.stories.tsx} | 33 +- .../notice-table.js => src/notice-table.tsx} | 82 +- .../src/radio-button-group.stories.tsx | 35 + .../radio-button-group.tsx} | 48 +- .../insomnia-components/src/sidebar/index.tsx | 18 + .../sidebar/sidebar-badge.tsx} | 23 +- .../sidebar/sidebar-filter.tsx} | 29 +- .../sidebar/sidebar-header.tsx} | 39 +- .../sidebar/sidebar-headers.tsx} | 31 +- .../src/sidebar/sidebar-info.tsx | 55 + .../src/sidebar/sidebar-invalid-section.tsx | 15 + .../sidebar/sidebar-item.tsx} | 21 +- .../src/sidebar/sidebar-panel.tsx | 29 + .../sidebar/sidebar-parameters.tsx} | 31 +- .../sidebar/sidebar-paths.tsx} | 44 +- .../sidebar/sidebar-requests.tsx} | 39 +- .../sidebar/sidebar-responses.tsx} | 27 +- .../sidebar/sidebar-schemas.tsx} | 25 +- .../sidebar/sidebar-section.tsx} | 33 +- .../sidebar/sidebar-security.tsx} | 29 +- .../sidebar/sidebar-servers.tsx} | 35 +- .../sidebar/sidebar-text-item.tsx} | 17 +- .../sidebar/sidebar.stories.tsx} | 99 +- .../index.js => src/sidebar/sidebar.tsx} | 116 +- .../svg-icon.stories.tsx} | 22 +- packages/insomnia-components/src/svg-icon.tsx | 220 + .../src/switch.stories.tsx | 36 + .../{components/switch.js => src/switch.tsx} | 35 +- .../table.stories.tsx} | 7 +- packages/insomnia-components/src/table.tsx | 112 + .../toggle-switch.stories.tsx} | 15 +- .../toggle-switch.tsx} | 46 +- .../tooltip.stories.tsx} | 13 +- .../tooltip.test.js => src/tooltip.test.tsx} | 16 +- .../tooltip.js => src/tooltip.tsx} | 130 +- packages/insomnia-components/svgr.config.js | 24 + .../insomnia-components/tsconfig.build.json | 19 + packages/insomnia-components/tsconfig.json | 16 + .../webpack/webpack.common.js | 25 +- .../webpack/webpack.dev.js | 7 +- .../webpack/webpack.prod.js | 5 +- packages/insomnia-cookies/.eslintignore | 3 + packages/insomnia-cookies/jest.config.js | 17 + packages/insomnia-cookies/package.json | 16 +- .../index.test.js => src/cookies.test.ts} | 17 +- .../{index.js => src/cookies.ts} | 35 +- packages/insomnia-cookies/src/index.ts | 1 + .../insomnia-cookies/src/tough-cookie.d.ts | 244 + packages/insomnia-cookies/tsconfig.build.json | 15 + packages/insomnia-cookies/tsconfig.json | 14 + packages/insomnia-importers/.eslintignore | 2 + packages/insomnia-importers/.eslintrc.js | 8 + packages/insomnia-importers/README.md | 4 +- packages/insomnia-importers/bin/cli | 2 +- packages/insomnia-importers/index.js | 45 - packages/insomnia-importers/jest.config.js | 17 + packages/insomnia-importers/package-lock.json | 274 +- packages/insomnia-importers/package.json | 29 +- .../insomnia-importers/src/{cli.js => cli.ts} | 24 +- .../import-errors.test.js => convert.test.ts} | 6 +- packages/insomnia-importers/src/convert.ts | 49 + packages/insomnia-importers/src/entities.ts | 101 + .../src/importers/apiconnect-wsdl.d.ts | 62 + .../insomnia-importers/src/importers/curl.js | 258 - .../insomnia-importers/src/importers/curl.ts | 344 + .../fixtures/curl/complex-input.sh | 0 .../fixtures/curl/complex-output.json | 0 .../fixtures/curl/dollar-sign-input.sh | 0 .../fixtures/curl/dollar-sign-output.json | 0 .../fixtures/curl/form-input.sh | 0 .../fixtures/curl/form-output.json | 0 .../fixtures/curl/from-chrome-input.sh | 0 .../fixtures/curl/from-chrome-output.json | 0 .../fixtures/curl/get-input.sh | 0 .../fixtures/curl/get-output.json | 0 .../fixtures/curl/header-colon-input.sh | 0 .../fixtures/curl/header-colon-output.json | 0 .../fixtures/curl/multi-data-input.sh | 0 .../fixtures/curl/multi-data-output.json | 0 .../fixtures/curl/multi-input.sh | 0 .../fixtures/curl/multi-output.json | 0 .../fixtures/curl/no-url-input.sh | 0 .../fixtures/curl/no-url-output.json | 0 .../fixtures/curl/question-mark-input.sh | 0 .../fixtures/curl/question-mark-output.json | 0 .../fixtures/curl/simple-url-input.sh | 0 .../fixtures/curl/simple-url-output.json | 0 .../fixtures/curl/skip-squished-input.sh | 0 .../fixtures/curl/squished-output.json | 0 .../fixtures/curl/url-only-input.sh | 0 .../fixtures/curl/url-only-output.json | 0 .../fixtures/curl/urlencoded-input.sh | 0 .../fixtures/curl/urlencoded-output.json | 0 .../fixtures/har/deep-input.json | 0 .../fixtures/har/deep-output.json | 0 .../fixtures/har/form-data-input.json | 0 .../fixtures/har/form-data-output.json | 0 .../fixtures/har/minimal-input.json | 0 .../fixtures/har/minimal-output.json | 0 .../fixtures/har/no-requests-input.json | 0 .../fixtures/har/no-requests-output.json | 0 .../fixtures/har/shallow-input.json | 0 .../fixtures/har/shallow-output.json | 0 .../fixtures/insomnia-1/complex-input.json | 0 .../fixtures/insomnia-1/complex-output.json | 0 .../fixtures/insomnia-1/form-input.json | 0 .../fixtures/insomnia-1/form-output.json | 0 .../fixtures/insomnia-1/minimal-input.json | 0 .../fixtures/insomnia-1/minimal-output.json | 0 .../fixtures/insomnia-2/complex-input.json | 0 .../fixtures/insomnia-2/complex-output.json | 0 .../fixtures/insomnia-3/basic-input.json | 0 .../fixtures/insomnia-3/basic-output.json | 0 .../fixtures/insomnia-4/basic-input.yaml | 0 .../fixtures/insomnia-4/basic-output.json | 0 .../fixtures/openapi3/dereferenced-input.json | 0 .../openapi3/dereferenced-output.json | 0 .../dereferenced-with-tags-input.json | 0 .../dereferenced-with-tags-output.json | 0 .../openapi3/endpoint-security-input.yaml | 0 .../openapi3/endpoint-security-output.json | 0 .../example-with-server-variables-input.yaml | 0 .../example-with-server-variables-output.json | 0 .../example-without-servers-input.yaml | 0 .../example-without-servers-output.json | 0 .../openapi3/global-security-input.yaml | 0 .../openapi3/global-security-output.json | 0 .../fixtures/openapi3/path-plugin-input.yaml | 0 .../fixtures/openapi3/path-plugin-output.json | 0 .../fixtures/openapi3/petstore-input.json | 0 .../fixtures/openapi3/petstore-output.json | 0 .../openapi3/petstore-readonly-input.yml | 0 .../openapi3/petstore-readonly-output.json | 0 .../openapi3/petstore-with-tags-input.json | 0 .../openapi3/petstore-with-tags-output.json | 0 .../fixtures/openapi3/petstore-yml-input.yml | 0 .../openapi3/petstore-yml-output.json | 0 .../openapi3/petstore-yml-with-tags-input.yml | 0 .../petstore-yml-with-tags-output.json | 0 .../fixtures/postman-env/basic-input.json | 0 .../fixtures/postman-env/basic-output.json | 0 .../fixtures/postman-env/no-name-input.json | 0 .../fixtures/postman-env/no-name-output.json | 0 .../aws-signature-auth-v2_0-input.json | 0 .../aws-signature-auth-v2_0-output.json | 0 .../aws-signature-auth-v2_1-input.json | 0 .../aws-signature-auth-v2_1-output.json | 0 .../postman/basic-auth-v2_0-input.json | 0 .../postman/basic-auth-v2_0-output.json | 0 .../postman/basic-auth-v2_1-input.json | 0 .../postman/basic-auth-v2_1-output.json | 0 .../postman/bearer-token-v2_0-input.json | 0 .../postman/bearer-token-v2_0-output.json | 0 .../postman/bearer-token-v2_1-input.json | 0 .../postman/bearer-token-v2_1-output.json | 0 .../postman/complex-url-v2_0-input.json | 0 .../postman/complex-url-v2_0-output.json | 0 .../postman/complex-url-v2_1-input.json | 0 .../postman/complex-url-v2_1-output.json | 0 .../fixtures/postman/complex-v2_0-input.json | 0 .../fixtures/postman/complex-v2_0-output.json | 0 .../fixtures/postman/complex-v2_1-input.json | 0 .../fixtures/postman/complex-v2_1-output.json | 0 .../postman/digest-auth-v2_0-input.json | 0 .../postman/digest-auth-v2_0-output.json | 0 .../postman/digest-auth-v2_1-input.json | 0 .../postman/digest-auth-v2_1-output.json | 0 .../fixtures/postman/minimal-v2_0-input.json | 0 .../fixtures/postman/minimal-v2_0-output.json | 0 .../fixtures/postman/minimal-v2_1-input.json | 0 .../fixtures/postman/minimal-v2_1-output.json | 0 .../postman/oauth1_0-auth-v2_0-input.json | 0 .../postman/oauth1_0-auth-v2_0-output.json | 0 .../postman/oauth1_0-auth-v2_1-input.json | 0 .../postman/oauth1_0-auth-v2_1-output.json | 0 .../postman/oauth2_0-auth-v2_0-input.json | 0 .../postman/oauth2_0-auth-v2_0-output.json | 0 .../postman/oauth2_0-auth-v2_1-input.json | 0 .../postman/oauth2_0-auth-v2_1-output.json | 0 .../fixtures/swagger2/dereferenced-input.json | 0 .../swagger2/dereferenced-output.json | 0 .../dereferenced-with-tags-input.json | 0 .../dereferenced-with-tags-output.json | 0 .../fixtures/swagger2/petstore-input.json | 0 .../fixtures/swagger2/petstore-output.json | 0 .../swagger2/petstore-with-tags-input.json | 0 .../swagger2/petstore-with-tags-output.json | 0 .../fixtures/swagger2/petstore-yml-input.yml | 0 .../swagger2/petstore-yml-output.json | 0 .../swagger2/petstore-yml-with-tags-input.yml | 0 .../petstore-yml-with-tags-output.json | 0 .../fixtures/swagger2/user-example-input.json | 0 .../swagger2/user-example-output.json | 0 .../fixtures/wsdl/addition-input.wsdl | 0 .../fixtures/wsdl/addition-output.json | 0 .../fixtures/wsdl/calculator-input.wsdl | 0 .../fixtures/wsdl/calculator-output.json | 0 .../insomnia-importers/src/importers/har.js | 137 - .../insomnia-importers/src/importers/har.ts | 119 + .../index.test.ts} | 48 +- .../insomnia-importers/src/importers/index.ts | 32 + .../src/importers/insomnia-1.js | 118 - .../src/importers/insomnia-1.ts | 170 + .../src/importers/insomnia-2.js | 37 - .../src/importers/insomnia-2.ts | 46 + .../src/importers/insomnia-3.js | 22 - .../src/importers/insomnia-3.ts | 28 + .../src/importers/insomnia-4.js | 24 - .../src/importers/insomnia-4.ts | 28 + .../src/importers/openapi-3.ts | 744 + .../src/importers/openapi3.js | 717 - .../src/importers/postman-2.0.types.ts | 657 + .../src/importers/postman-2.1.types.ts | 646 + .../src/importers/postman-env.js | 38 - .../src/importers/postman-env.ts | 54 + .../src/importers/postman.js | 411 - .../src/importers/postman.ts | 513 + .../src/importers/swagger-2.ts | 589 + .../src/importers/swagger2.js | 521 - .../src/importers/{wsdl.js => wsdl.ts} | 119 +- packages/insomnia-importers/src/index.ts | 3 + packages/insomnia-importers/src/utils.js | 64 - .../utils.test.js => utils.test.ts} | 47 +- packages/insomnia-importers/src/utils.ts | 49 + .../insomnia-importers/tsconfig.build.json | 19 + packages/insomnia-importers/tsconfig.json | 18 + packages/insomnia-inso/.babelrc | 16 - packages/insomnia-inso/.eslintignore | 3 + packages/insomnia-inso/.eslintrc.js | 7 + packages/insomnia-inso/.flowconfig | 14 - packages/insomnia-inso/README.md | 16 +- packages/insomnia-inso/__jest__/before.js | 27 - .../insomnia-inso/__mocks__/cosmiconfig.js | 5 - packages/insomnia-inso/__mocks__/enquirer.js | 20 - .../__mocks__/insomnia-send-request.js | 3 - .../__mocks__/insomnia-testing.js | 8 - packages/insomnia-inso/bin/inso | 3 +- .../flow-typed/@stoplight/spectral.js | 5 - .../insomnia-inso/flow-typed/commander.js | 5 - packages/insomnia-inso/flow-typed/consola.js | 5 - .../insomnia-inso/flow-typed/cosmiconfig.js | 5 - packages/insomnia-inso/flow-typed/enquirer.js | 5 - packages/insomnia-inso/flow-typed/execa.js | 5 - .../insomnia-inso/flow-typed/get-bin-path.js | 5 - .../flow-typed/insomnia-send-request.js | 11 - .../flow-typed/insomnia-testing.js | 5 - packages/insomnia-inso/flow-typed/jest.js | 1186 -- .../flow-typed/lodash.flattendeep.js | 5 - packages/insomnia-inso/flow-typed/mkdirp.js | 7 - packages/insomnia-inso/flow-typed/nedb.js | 5 - .../flow-typed/openapi-2-kong.js | 38 - .../insomnia-inso/flow-typed/string-argv.js | 5 - packages/insomnia-inso/flow-typed/yaml.js | 9 - packages/insomnia-inso/jest.config.js | 35 + packages/insomnia-inso/package-lock.json | 2747 +-- packages/insomnia-inso/package.json | 60 +- .../src/__mocks__/cosmiconfig.ts | 4 + .../src/__mocks__/{util.js => util.ts} | 1 - .../inso-snapshot.test.ts.snap} | 4 +- .../{__tests__/cli.test.js => cli.test.ts} | 86 +- packages/insomnia-inso/src/{cli.js => cli.ts} | 140 +- .../__mocks__/insomnia-send-request.ts | 1 + .../commands/__mocks__/insomnia-testing.ts | 4 + .../commands/__mocks__/openapi-2-kong.ts} | 4 +- ...n.test.js => export-specification.test.ts} | 34 +- ...ecification.js => export-specification.ts} | 18 +- .../openapi-spec.yaml | 0 ...config.test.js => generate-config.test.ts} | 96 +- ...{generate-config.js => generate-config.ts} | 59 +- ...ion.test.js => lint-specification.test.ts} | 36 +- ...specification.js => lint-specification.ts} | 24 +- .../run-tests.test.js => run-tests.test.ts} | 58 +- .../commands/{run-tests.js => run-tests.ts} | 70 +- ...rectory.test.js => data-directory.test.ts} | 43 +- .../{data-directory.js => data-directory.ts} | 12 +- .../src/db/__mocks__/{index.js => index.ts} | 3 - ...it-adapter.test.js => git-adapter.test.ts} | 8 +- .../{git-adapter.js => git-adapter.ts} | 27 +- .../src/db/adapters/ne-db-adapter.js | 44 - ...-adapter.test.js => ne-db-adapter.test.ts} | 8 +- .../src/db/adapters/ne-db-adapter.ts | 38 + .../spc_46c5a4a40e83445a9bd9d9758b86c16c.yml | 0 ...046a738f001eb3090261a537b1b78f86c2094c.yml | 0 ...738f001eb3090261a537b1b78f86c2094c_sub.yml | 0 ...2d4860c7da418a85ffea7406e1292a21946b60.yml | 0 ...2d4860c7da418a85ffea7406e1292ab410454b.yml | 0 ...2d4860c7da418a85ffea7406e1292a30baa249.yml | 0 .../wrk_012d4860c7da418a85ffea7406e1292a.yml | 0 .../spc_46c5a4a40e83445a9bd9d9758b86c16c.yml | 0 ...046a738f001eb3090261a537b1b78f86c2094c.yml | 0 ...738f001eb3090261a537b1b78f86c2094c_sub.yml | 0 ...2d4860c7da418a85ffea7406e1292a21946b60.yml | 0 ...2d4860c7da418a85ffea7406e1292ab410454b.yml | 0 ...2d4860c7da418a85ffea7406e1292a30baa249.yml | 0 .../ut_6b214a8cd07046f2a8f43237ca913c73.yml | 0 .../ut_8c0c020fb9e341c2a3162e723445faf1.yml | 0 .../ut_8fc42ec7f82a45d09e691ddfe212fd85.yml | 0 .../ut_97a197104e83454ebf5f4e0dc0ba4bc6.yml | 0 .../uts_7f0f85548b0147f4ba6b5e442d137613.yml | 0 .../uts_fe901c6565044f00aa620d3fe47f443f.yml | 0 .../wrk_012d4860c7da418a85ffea7406e1292a.yml | 0 .../nedb/insomnia.ApiSpec.db | 0 .../nedb/insomnia.Environment.db | 0 .../nedb/insomnia.Request.db | 0 .../nedb/insomnia.RequestGroup.db | 0 .../nedb/insomnia.UnitTest.db | 0 .../nedb/insomnia.UnitTestSuite.db | 0 .../nedb/insomnia.Workspace.db | 0 .../index.test.js => index.test.ts} | 51 +- .../src/db/{index.js => index.ts} | 46 +- .../src/db/models/__mocks__/enquirer.ts | 20 + .../api-spec.test.ts.snap} | 0 .../environment.test.ts.snap} | 0 .../unit-test-suite.test.ts.snap} | 0 .../insomnia-inso/src/db/models/api-spec.js | 32 - .../api-spec.test.js => api-spec.test.ts} | 27 +- .../insomnia-inso/src/db/models/api-spec.ts | 46 + ...nvironment.test.js => environment.test.ts} | 43 +- .../models/{environment.js => environment.ts} | 35 +- packages/insomnia-inso/src/db/models/types.js | 45 - packages/insomnia-inso/src/db/models/types.ts | 42 + ...-suite.test.js => unit-test-suite.test.ts} | 33 +- ...{unit-test-suite.js => unit-test-suite.ts} | 50 +- packages/insomnia-inso/src/db/models/util.js | 54 - packages/insomnia-inso/src/db/models/util.ts | 63 + .../insomnia-inso/src/db/models/workspace.js | 13 - .../workspace.test.js => workspace.test.ts} | 21 +- .../insomnia-inso/src/db/models/workspace.ts | 15 + .../src/{errors.js => errors.ts} | 7 +- .../src/fixtures/.insorc-blank.yaml | 0 .../.insorc-missing-properties.yaml | 0 .../.insorc-with-scripts.yaml | 0 .../{__fixtures__ => fixtures}/.insorc.yaml | 0 ...et-options.test.js => get-options.test.ts} | 95 +- .../src/{get-options.js => get-options.ts} | 58 +- packages/insomnia-inso/src/global.d.ts | 8 + packages/insomnia-inso/src/index.ts | 1 + ...snapshot.test.js => inso-snapshot.test.ts} | 11 +- .../jest/__mocks__/node-libcurl.ts} | 2 +- packages/insomnia-inso/src/jest/before.ts | 35 + .../{__jest__/setup.js => src/jest/setup.ts} | 1 - packages/insomnia-inso/src/logger.js | 24 - .../logger.test.js => logger.test.ts} | 11 +- packages/insomnia-inso/src/logger.ts | 43 + .../insomnia-inso/src/openapi-2-kong.d.ts | 57 + packages/insomnia-inso/src/types.ts | 8 + .../{__tests__/util.test.js => util.test.ts} | 54 +- .../insomnia-inso/src/{util.js => util.ts} | 26 +- .../write-file.test.js => write-file.test.ts} | 34 +- .../src/{write-file.js => write-file.ts} | 2 +- packages/insomnia-inso/tsconfig.build.json | 27 + packages/insomnia-inso/tsconfig.json | 14 + .../webpack/webpack.config.base.js | 22 +- .../webpack/webpack.config.development.js | 3 +- .../webpack/webpack.config.production.js | 3 +- packages/insomnia-prettify/.eslintignore | 1 + packages/insomnia-prettify/.eslintrc.js | 7 + packages/insomnia-prettify/index.js | 3 - packages/insomnia-prettify/jest.config.js | 17 + packages/insomnia-prettify/package-lock.json | 4866 +++++ packages/insomnia-prettify/package.json | 14 +- .../escaped-characters-input.json | 0 .../escaped-characters-output.json | 0 .../escaped-unicode-input.json | 0 .../escaped-unicode-output.json | 0 .../extra-whitespace-input.json | 0 .../extra-whitespace-output.json | 0 .../minimal-whitespace-input.json | 0 .../minimal-whitespace-output.json | 0 .../nunjucks-input.json | 0 .../nunjucks-output.json | 0 .../precision-input.json | 0 .../precision-output.json | 0 .../root-array-input.json | 0 .../root-array-output.json | 0 .../root-string-input.json | 0 .../root-string-output.json | 0 .../trailing-comma-input.json | 0 .../trailing-comma-output.json | 0 .../unquoted-strings-input.json | 0 .../unquoted-strings-output.json | 0 packages/insomnia-prettify/src/index.ts | 1 + .../{__tests__/json.test.js => json.test.ts} | 8 +- .../src/{json.js => json.ts} | 55 +- .../insomnia-prettify/tsconfig.build.json | 15 + packages/insomnia-prettify/tsconfig.json | 15 + packages/insomnia-send-request/package.json | 1 + packages/insomnia-smoke-test/core/app.test.js | 5 +- packages/insomnia-smoke-test/modules/debug.js | 2 + .../insomnia-smoke-test/modules/dropdown.js | 10 +- packages/insomnia-smoke-test/modules/home.js | 12 +- .../insomnia-smoke-test/package-lock.json | 1616 +- packages/insomnia-smoke-test/package.json | 2 +- packages/insomnia-testing/.babelrc | 16 - packages/insomnia-testing/.eslintignore | 2 + packages/insomnia-testing/.eslintrc.js | 7 + packages/insomnia-testing/.flowconfig | 15 - packages/insomnia-testing/flow-typed/axios.js | 9 - packages/insomnia-testing/flow-typed/chai.js | 5 - packages/insomnia-testing/flow-typed/jest.js | 1186 -- .../insomnia-testing/flow-typed/mkdirp.js | 7 - packages/insomnia-testing/flow-typed/mocha.js | 25 - packages/insomnia-testing/index.js | 3 - packages/insomnia-testing/jest.config.js | 20 + packages/insomnia-testing/package-lock.json | 4862 +---- packages/insomnia-testing/package.json | 29 +- .../insomnia-testing/src/__mocks__/axios.js | 27 - .../insomnia-testing/src/__mocks__/axios.ts | 47 + .../01_empty.input.json | 0 .../01_empty.output.js | 0 .../02_empty-suite.input.json | 0 .../02_empty-suite.output.js | 0 .../03_basic-suite.input.json | 0 .../03_basic-suite.output.js | 0 .../04_nested-suite.input.json | 0 .../04_nested-suite.output.js | 0 .../05_complex-suite.input.json | 0 .../05_complex-suite.output.js | 0 .../fixtures.test.js => generate.test.ts} | 18 +- .../src/generate/{index.js => generate.ts} | 81 +- .../insomnia-testing/src/generate/index.ts | 6 + .../{__tests__/util.test.js => util.test.ts} | 12 +- .../src/generate/{util.js => util.ts} | 12 +- packages/insomnia-testing/src/index.ts | 12 + .../integration.test.ts} | 54 +- packages/insomnia-testing/src/run/entities.ts | 32 + packages/insomnia-testing/src/run/index.js | 144 - packages/insomnia-testing/src/run/index.ts | 8 + packages/insomnia-testing/src/run/insomnia.js | 120 - packages/insomnia-testing/src/run/insomnia.ts | 124 + .../src/run/java-script-reporter.js | 111 - .../src/run/javascript-reporter.ts | 105 + .../{__tests__/index.test.js => run.test.ts} | 29 +- packages/insomnia-testing/src/run/run.ts | 113 + packages/insomnia-testing/tsconfig.build.json | 16 + packages/insomnia-testing/tsconfig.json | 15 + packages/insomnia-testing/webpack.config.js | 21 +- packages/insomnia-url/.eslintignore | 1 + packages/insomnia-url/.eslintrc.js | 7 + packages/insomnia-url/index.js | 12 - packages/insomnia-url/jest.config.js | 17 + packages/insomnia-url/package-lock.json | 4866 +++++ packages/insomnia-url/package.json | 14 +- packages/insomnia-url/src/index.ts | 11 + .../protocol.test.js => protocol.test.ts} | 2 +- .../src/{protocol.js => protocol.ts} | 7 +- ...uerystring.test.js => querystring.test.ts} | 9 +- .../src/{querystring.js => querystring.ts} | 95 +- packages/insomnia-url/tsconfig.build.json | 15 + packages/insomnia-url/tsconfig.json | 15 + packages/insomnia-xpath/.eslintignore | 1 + .../insomnia-xpath/__tests__/index.test.js | 36 - packages/insomnia-xpath/index.js | 45 - packages/insomnia-xpath/jest.config.js | 17 + packages/insomnia-xpath/package-lock.json | 6 + packages/insomnia-xpath/package.json | 17 +- packages/insomnia-xpath/src/index.ts | 1 + packages/insomnia-xpath/src/query.test.ts | 34 + packages/insomnia-xpath/src/query.ts | 53 + packages/insomnia-xpath/tsconfig.build.json | 15 + packages/insomnia-xpath/tsconfig.json | 13 + packages/openapi-2-kong/.babelrc | 16 - packages/openapi-2-kong/.eslintignore | 1 + packages/openapi-2-kong/.eslintrc.js | 10 + packages/openapi-2-kong/.flowconfig | 16 - packages/openapi-2-kong/flow-typed/jest.js | 1186 -- packages/openapi-2-kong/flow-typed/slugify.js | 5 - .../flow-typed/swagger-parser.js | 11 - .../openapi-2-kong/flow-typed/url-join.js | 5 - packages/openapi-2-kong/jest.config.js | 19 + packages/openapi-2-kong/package-lock.json | 2661 +-- packages/openapi-2-kong/package.json | 28 +- .../common.test.js => common.test.ts} | 209 +- .../src/{common.js => common.ts} | 61 +- .../{index.js => generate.ts} | 6 +- .../src/declarative-config/index.ts | 1 + .../names.test.js => names.test.ts} | 19 +- .../plugins.test.js => plugins.test.ts} | 109 +- .../{plugins.js => plugins.ts} | 100 +- ...ugins.test.js => security-plugins.test.ts} | 9 +- ...ecurity-plugins.js => security-plugins.ts} | 83 +- .../services.test.js => services.test.ts} | 179 +- .../{services.js => services.ts} | 34 +- .../upstreams.test.js => upstreams.test.ts} | 11 +- .../{upstreams.js => upstreams.ts} | 13 +- .../{__tests__/utils.js => utils.ts} | 15 +- .../fixtures.test.js => fixtures.test.ts} | 14 +- .../api-with-examples.expected.json | 0 .../api-with-examples.yaml | 0 .../callback-example.expected.json | 0 .../callback-example.yaml | 0 .../httpbin.expected.json | 0 .../{__fixtures__ => fixtures}/httpbin.yaml | 0 .../link-example.expected.json | 0 .../link-example.yaml | 0 .../no-targets-example.expected.json | 0 .../no-targets-example.yaml | 0 .../petstore-expanded.expected.json | 0 .../petstore-expanded.yaml | 0 .../petstore.expected.json | 0 .../{__fixtures__ => fixtures}/petstore.yaml | 0 .../request-validator-plugin.expected.json | 0 .../request-validator-plugin.yaml | 0 .../security.expected.json | 0 .../{__fixtures__ => fixtures}/security.yaml | 0 .../uspto.expected.json | 0 .../src/{__fixtures__ => fixtures}/uspto.yaml | 0 .../index.test.js => generate.test.ts} | 22 +- .../src/{index.js => generate.ts} | 35 +- packages/openapi-2-kong/src/index.ts | 6 + .../index.test.js => generate.test.ts} | 186 +- .../src/kubernetes/{index.js => generate.ts} | 94 +- .../openapi-2-kong/src/kubernetes/index.ts | 10 + .../plugin-helpers.js => plugin-helpers.ts} | 56 +- .../plugins.test.js => plugins.test.ts} | 229 +- .../src/kubernetes/{plugins.js => plugins.ts} | 97 +- .../variables.test.js => variables.test.ts} | 23 +- .../kubernetes/{variables.js => variables.ts} | 7 +- .../src/types/declarative-config.ts | 56 + .../openapi-2-kong/src/types/k8plugins.ts | 27 + .../src/types/kubernetes-config.ts | 67 + packages/openapi-2-kong/src/types/openapi3.ts | 248 + packages/openapi-2-kong/src/types/outputs.ts | 32 + packages/openapi-2-kong/tsconfig.build.json | 16 + packages/openapi-2-kong/tsconfig.json | 15 + .../types/declarative-config.flow.js | 47 - .../openapi-2-kong/types/k8plugins.flow.js | 25 - .../types/kubernetes-config.flow.js | 62 - .../openapi-2-kong/types/openapi3.flow.js | 240 - packages/openapi-2-kong/types/outputs.flow.js | 31 - packages/openapi-2-kong/webpack.config.js | 25 - .../package-lock.json | 4 +- .../package-lock.json | 122 + .../insomnia-plugin-kong-portal/package.json | 5 +- scripts/check-version.js | 1 + scripts/convert-to-typescript | 64 + tsconfig.base.json | 24 + tsconfig.eslint.json | 7 + 1285 files changed, 60440 insertions(+), 44623 deletions(-) create mode 100644 .eslintrc.js delete mode 100644 .eslintrc.json delete mode 100644 .prettierignore delete mode 100644 .prettierrc create mode 100644 jest.config.js delete mode 100644 packages/insomnia-app/.babelrc create mode 100644 packages/insomnia-app/.eslintignore create mode 100644 packages/insomnia-app/.eslintrc.js delete mode 100644 packages/insomnia-app/.flowconfig rename packages/insomnia-app/app/__jest__/{before-each.js => before-each.ts} (59%) rename packages/insomnia-app/app/__jest__/{redux-state-for-test.js => redux-state-for-test.ts} (77%) rename packages/insomnia-app/app/__jest__/{setup-after-env.js => setup-after-env.ts} (58%) delete mode 100644 packages/insomnia-app/app/__jest__/setup.js create mode 100644 packages/insomnia-app/app/__jest__/setup.ts rename packages/insomnia-app/app/__mocks__/@grpc/{grpc-js.js => grpc-js.ts} (95%) delete mode 100644 packages/insomnia-app/app/__mocks__/dummy.js create mode 100644 packages/insomnia-app/app/__mocks__/dummy.ts rename packages/insomnia-app/app/__mocks__/{electron.js => electron.ts} (56%) delete mode 100644 packages/insomnia-app/app/__mocks__/font-scanner.js create mode 100644 packages/insomnia-app/app/__mocks__/font-scanner.ts rename packages/insomnia-app/app/__mocks__/{isomorphic-git.js => isomorphic-git.ts} (54%) rename packages/insomnia-app/app/__mocks__/{node-forge.js => node-forge.ts} (79%) rename packages/insomnia-app/app/__mocks__/{node-libcurl.js => node-libcurl.ts} (61%) rename packages/insomnia-app/app/__tests__/{install.test.js => install.test.ts} (94%) rename packages/insomnia-app/app/__tests__/{package.test.js => package.test.ts} (99%) rename packages/insomnia-app/app/__tests__/{renderer.test.js => renderer.test.ts} (99%) rename packages/insomnia-app/app/account/__tests__/{crypt.test.js => crypt.test.ts} (99%) rename packages/insomnia-app/app/account/{crypt.js => crypt.ts} (86%) rename packages/insomnia-app/app/account/{fetch.js => fetch.ts} (91%) rename packages/insomnia-app/app/account/{session.js => session.ts} (95%) rename packages/insomnia-app/app/common/__fixtures__/{nestedfolders.js => nestedfolders.ts} (77%) rename packages/insomnia-app/app/common/__mocks__/{analytics.js => analytics.ts} (50%) delete mode 100644 packages/insomnia-app/app/common/__mocks__/render.js create mode 100644 packages/insomnia-app/app/common/__mocks__/render.ts rename packages/insomnia-app/app/common/__tests__/{analytics.test.js => analytics.test.ts} (99%) rename packages/insomnia-app/app/common/__tests__/{api-specs.test.js => api-specs.test.ts} (93%) rename packages/insomnia-app/app/common/__tests__/{constants.test.js => constants.test.ts} (100%) rename packages/insomnia-app/app/common/__tests__/{database.test.js => database.test.ts} (67%) rename packages/insomnia-app/app/common/__tests__/{grpc-paths.test.js => grpc-paths.test.ts} (92%) rename packages/insomnia-app/app/common/__tests__/{har.test.js => har.test.ts} (71%) rename packages/insomnia-app/app/common/__tests__/{import.test.js => import.test.ts} (72%) rename packages/insomnia-app/app/common/__tests__/{local-storage.test.js => local-storage.test.ts} (93%) rename packages/insomnia-app/app/common/__tests__/{misc.test.js => misc.test.ts} (77%) rename packages/insomnia-app/app/common/__tests__/{render.test.js => render.test.ts} (94%) delete mode 100644 packages/insomnia-app/app/common/__tests__/sorting.test.js create mode 100644 packages/insomnia-app/app/common/__tests__/sorting.test.ts rename packages/insomnia-app/app/common/__tests__/{strings.test.js => strings.test.ts} (98%) rename packages/insomnia-app/app/common/{analytics.js => analytics.ts} (72%) rename packages/insomnia-app/app/common/{api-specs.js => api-specs.ts} (79%) rename packages/insomnia-app/app/common/{constants.js => constants.ts} (87%) delete mode 100644 packages/insomnia-app/app/common/database.js create mode 100644 packages/insomnia-app/app/common/database.ts rename packages/insomnia-app/app/common/{documentation.js => documentation.ts} (65%) create mode 100644 packages/insomnia-app/app/common/electron-helpers.ts rename packages/insomnia-app/app/common/{get-workspace-label.js => get-workspace-label.ts} (96%) rename packages/insomnia-app/app/common/{grpc-events.js => grpc-events.ts} (64%) rename packages/insomnia-app/app/common/{grpc-paths.js => grpc-paths.ts} (78%) rename packages/insomnia-app/app/common/{har.js => har.ts} (60%) rename packages/insomnia-app/app/common/{hotkeys-listener.js => hotkeys-listener.ts} (58%) rename packages/insomnia-app/app/common/{hotkeys.js => hotkeys.ts} (94%) rename packages/insomnia-app/app/common/{import.js => import.ts} (78%) rename packages/insomnia-app/app/common/{keyboard-keys.js => keyboard-keys.ts} (99%) rename packages/insomnia-app/app/common/{log.js => log.ts} (82%) rename packages/insomnia-app/app/common/{markdown-to-html.js => markdown-to-html.ts} (81%) rename packages/insomnia-app/app/common/{migrate-from-designer.js => migrate-from-designer.ts} (81%) rename packages/insomnia-app/app/common/{misc.js => misc.ts} (62%) rename packages/insomnia-app/app/common/{render.js => render.ts} (91%) rename packages/insomnia-app/app/common/{select-file-or-folder.js => select-file-or-folder.ts} (57%) rename packages/insomnia-app/app/common/{send-request.js => send-request.ts} (77%) rename packages/insomnia-app/app/common/{sorting.js => sorting.ts} (92%) rename packages/insomnia-app/app/common/{strings.js => strings.ts} (96%) rename packages/insomnia-app/app/datasets/{access-token-urls.js => access-token-urls.ts} (100%) rename packages/insomnia-app/app/datasets/{authorization-urls.js => authorization-urls.ts} (100%) rename packages/insomnia-app/app/datasets/{charsets.js => charsets.ts} (100%) rename packages/insomnia-app/app/datasets/{content-types.js => content-types.ts} (100%) rename packages/insomnia-app/app/datasets/{encodings.js => encodings.ts} (100%) rename packages/insomnia-app/app/datasets/{header-names.js => header-names.ts} (100%) create mode 100644 packages/insomnia-app/app/global.d.ts rename packages/insomnia-app/app/{main.development.js => main.development.ts} (92%) rename packages/insomnia-app/app/main/__tests__/{grpc-ipc-main.test.js => grpc-ipc-main.test.ts} (97%) rename packages/insomnia-app/app/main/{error-handling.js => error-handling.ts} (79%) rename packages/insomnia-app/app/main/{grpc-ipc-main.js => grpc-ipc-main.ts} (88%) rename packages/insomnia-app/app/main/{local-storage.js => local-storage.ts} (83%) rename packages/insomnia-app/app/main/{squirrel-startup.js => squirrel-startup.ts} (91%) rename packages/insomnia-app/app/main/{updates.js => updates.ts} (82%) rename packages/insomnia-app/app/main/{window-utils.js => window-utils.ts} (65%) rename packages/insomnia-app/app/models/__mocks__/{uuid.js => uuid.ts} (98%) rename packages/insomnia-app/app/models/__tests__/{grpc-request-meta.test.js => grpc-request-meta.test.ts} (83%) rename packages/insomnia-app/app/models/__tests__/{grpc-request.test.js => grpc-request.test.ts} (88%) rename packages/insomnia-app/app/models/__tests__/{index.test.js => index.test.ts} (98%) rename packages/insomnia-app/app/models/__tests__/{proto-file.test.js => proto-file.test.ts} (89%) rename packages/insomnia-app/app/models/__tests__/{request-meta.test.js => request-meta.test.ts} (63%) rename packages/insomnia-app/app/models/__tests__/{request.test.js => request.test.ts} (73%) rename packages/insomnia-app/app/models/__tests__/{response.test.js => response.test.ts} (94%) rename packages/insomnia-app/app/models/__tests__/{workspace.test.js => workspace.test.ts} (78%) rename packages/insomnia-app/app/models/{api-spec.js => api-spec.ts} (50%) delete mode 100644 packages/insomnia-app/app/models/client-certificate.js create mode 100644 packages/insomnia-app/app/models/client-certificate.ts rename packages/insomnia-app/app/models/{cookie-jar.js => cookie-jar.ts} (53%) rename packages/insomnia-app/app/models/{environment.js => environment.ts} (55%) delete mode 100644 packages/insomnia-app/app/models/git-repository.js create mode 100644 packages/insomnia-app/app/models/git-repository.ts rename packages/insomnia-app/app/models/{grpc-request-meta.js => grpc-request-meta.ts} (58%) delete mode 100644 packages/insomnia-app/app/models/grpc-request.js create mode 100644 packages/insomnia-app/app/models/grpc-request.ts rename packages/insomnia-app/app/models/helpers/__tests__/{get-workspace-name.test.js => get-workspace-name.test.ts} (98%) rename packages/insomnia-app/app/models/helpers/__tests__/{git-repository-operations.test.js => git-repository-operations.test.ts} (94%) rename packages/insomnia-app/app/models/helpers/__tests__/{is-model.test.js => is-model.test.ts} (78%) rename packages/insomnia-app/app/models/helpers/__tests__/{query-all-workspace-urls.test.js => query-all-workspace-urls.test.ts} (91%) rename packages/insomnia-app/app/models/helpers/{get-workspace-name.js => get-workspace-name.ts} (91%) rename packages/insomnia-app/app/models/helpers/{git-repository-operations.js => git-repository-operations.ts} (86%) delete mode 100644 packages/insomnia-app/app/models/helpers/is-model.js create mode 100644 packages/insomnia-app/app/models/helpers/is-model.ts rename packages/insomnia-app/app/models/helpers/{query-all-workspace-urls.js => query-all-workspace-urls.ts} (53%) rename packages/insomnia-app/app/models/helpers/{request-operations.js => request-operations.ts} (58%) rename packages/insomnia-app/app/models/helpers/{workspace-operations.js => workspace-operations.ts} (55%) rename packages/insomnia-app/app/models/{index.js => index.ts} (88%) delete mode 100644 packages/insomnia-app/app/models/o-auth-2-token.js create mode 100644 packages/insomnia-app/app/models/o-auth-2-token.ts delete mode 100644 packages/insomnia-app/app/models/plugin-data.js create mode 100644 packages/insomnia-app/app/models/plugin-data.ts delete mode 100644 packages/insomnia-app/app/models/proto-directory.js create mode 100644 packages/insomnia-app/app/models/proto-directory.ts delete mode 100644 packages/insomnia-app/app/models/proto-file.js create mode 100644 packages/insomnia-app/app/models/proto-file.ts delete mode 100644 packages/insomnia-app/app/models/request-group-meta.js create mode 100644 packages/insomnia-app/app/models/request-group-meta.ts delete mode 100644 packages/insomnia-app/app/models/request-group.js create mode 100644 packages/insomnia-app/app/models/request-group.ts rename packages/insomnia-app/app/models/{request-meta.js => request-meta.ts} (62%) rename packages/insomnia-app/app/models/{request-version.js => request-version.ts} (70%) rename packages/insomnia-app/app/models/{request.js => request.ts} (76%) rename packages/insomnia-app/app/models/{response.js => response.ts} (65%) rename packages/insomnia-app/app/models/{settings.js => settings.ts} (54%) rename packages/insomnia-app/app/models/{stats.js => stats.ts} (54%) rename packages/insomnia-app/app/models/{unit-test-result.js => unit-test-result.ts} (62%) delete mode 100644 packages/insomnia-app/app/models/unit-test-suite.js create mode 100644 packages/insomnia-app/app/models/unit-test-suite.ts delete mode 100644 packages/insomnia-app/app/models/unit-test.js create mode 100644 packages/insomnia-app/app/models/unit-test.ts delete mode 100644 packages/insomnia-app/app/models/workspace-meta.js create mode 100644 packages/insomnia-app/app/models/workspace-meta.ts rename packages/insomnia-app/app/models/{workspace.js => workspace.ts} (68%) rename packages/insomnia-app/app/network/__tests__/{authentication.test.js => authentication.test.ts} (87%) rename packages/insomnia-app/app/network/__tests__/{certificate-url-parse.test.js => certificate-url-parse.test.ts} (99%) rename packages/insomnia-app/app/network/__tests__/{multipart.test.js => multipart.test.ts} (78%) rename packages/insomnia-app/app/network/__tests__/{network.test.js => network.test.ts} (83%) rename packages/insomnia-app/app/network/__tests__/{url-matches-cert-host.test.js => url-matches-cert-host.test.ts} (99%) rename packages/insomnia-app/app/network/{authentication.js => authentication.ts} (88%) rename packages/insomnia-app/app/network/{axios-request.js => axios-request.ts} (72%) rename packages/insomnia-app/app/network/basic-auth/__tests__/{get-header.test.js => get-header.test.ts} (99%) rename packages/insomnia-app/app/network/basic-auth/{get-header.js => get-header.ts} (55%) rename packages/insomnia-app/app/network/bearer-auth/{get-header.js => get-header.ts} (68%) rename packages/insomnia-app/app/network/{certificate-url-parse.js => certificate-url-parse.ts} (90%) delete mode 100644 packages/insomnia-app/app/network/grpc/__mocks__/index.js create mode 100644 packages/insomnia-app/app/network/grpc/__mocks__/index.ts rename packages/insomnia-app/app/network/grpc/__schemas__/{grpc-ipc-message-params-schema.js => grpc-ipc-message-params-schema.ts} (84%) rename packages/insomnia-app/app/network/grpc/__schemas__/{grpc-ipc-request-params-schema.js => grpc-ipc-request-params-schema.ts} (85%) rename packages/insomnia-app/app/network/grpc/__tests__/{call-cache.test.js => call-cache.test.ts} (76%) rename packages/insomnia-app/app/network/grpc/__tests__/{index.test.js => index.test.ts} (77%) rename packages/insomnia-app/app/network/grpc/__tests__/{method.test.js => method.test.ts} (68%) rename packages/insomnia-app/app/network/grpc/__tests__/{parse-grpc-url.test.js => parse-grpc-url.test.ts} (100%) rename packages/insomnia-app/app/network/grpc/__tests__/{prepare.test.js => prepare.test.ts} (72%) rename packages/insomnia-app/app/network/grpc/__tests__/{proto-integration.test.js => proto-integration.test.ts} (92%) rename packages/insomnia-app/app/network/grpc/__tests__/{response-callbacks.test.js => response-callbacks.test.ts} (94%) rename packages/insomnia-app/app/network/grpc/{call-cache.js => call-cache.ts} (66%) rename packages/insomnia-app/app/network/grpc/{index.js => index.ts} (86%) rename packages/insomnia-app/app/network/grpc/{method.js => method.ts} (67%) delete mode 100644 packages/insomnia-app/app/network/grpc/parse-grpc-url.js create mode 100644 packages/insomnia-app/app/network/grpc/parse-grpc-url.ts rename packages/insomnia-app/app/network/grpc/{prepare.js => prepare.ts} (69%) rename packages/insomnia-app/app/network/grpc/proto-loader/__mocks__/{index.js => index.ts} (100%) rename packages/insomnia-app/app/network/grpc/proto-loader/__tests__/{index.test.js => index.test.ts} (79%) rename packages/insomnia-app/app/network/grpc/proto-loader/__tests__/{write-proto-file.test.js => write-proto-file.test.ts} (97%) rename packages/insomnia-app/app/network/grpc/proto-loader/{index.js => index.ts} (65%) rename packages/insomnia-app/app/network/grpc/proto-loader/{write-proto-file.js => write-proto-file.ts} (87%) rename packages/insomnia-app/app/network/grpc/proto-manager/__tests__/{index.test.js => index.test.ts} (80%) rename packages/insomnia-app/app/network/grpc/proto-manager/__tests__/{ingest-proto-directory.test.js => ingest-proto-directory.test.ts} (85%) rename packages/insomnia-app/app/network/grpc/proto-manager/{index.js => index.tsx} (86%) rename packages/insomnia-app/app/network/grpc/proto-manager/{ingest-proto-directory.js => ingest-proto-directory.ts} (76%) rename packages/insomnia-app/app/network/grpc/{response-callbacks.js => response-callbacks.ts} (76%) rename packages/insomnia-app/app/network/grpc/{service-error.js => service-error.ts} (61%) rename packages/insomnia-app/app/network/{multipart.js => multipart.ts} (89%) rename packages/insomnia-app/app/network/{network.js => network.ts} (88%) rename packages/insomnia-app/app/network/o-auth-1/{constants.js => constants.ts} (97%) rename packages/insomnia-app/app/network/o-auth-1/{get-token.js => get-token.ts} (73%) rename packages/insomnia-app/app/network/o-auth-2/__tests__/{grant-authorization-code.test.js => grant-authorization-code.test.ts} (74%) rename packages/insomnia-app/app/network/o-auth-2/__tests__/{grant-client-credentials.test.js => grant-client-credentials.test.ts} (75%) rename packages/insomnia-app/app/network/o-auth-2/__tests__/{grant-implicit.test.js => grant-implicit.test.ts} (99%) rename packages/insomnia-app/app/network/o-auth-2/__tests__/{grant-password.test.js => grant-password.test.ts} (72%) rename packages/insomnia-app/app/network/o-auth-2/__tests__/{helpers.js => helpers.ts} (99%) rename packages/insomnia-app/app/network/o-auth-2/__tests__/{misc.test.js => misc.test.ts} (99%) rename packages/insomnia-app/app/network/o-auth-2/{constants.js => constants.ts} (99%) rename packages/insomnia-app/app/network/o-auth-2/{get-token.js => get-token.ts} (97%) rename packages/insomnia-app/app/network/o-auth-2/{grant-authorization-code.js => grant-authorization-code.ts} (62%) rename packages/insomnia-app/app/network/o-auth-2/{grant-client-credentials.js => grant-client-credentials.ts} (69%) rename packages/insomnia-app/app/network/o-auth-2/{grant-implicit.js => grant-implicit.ts} (64%) rename packages/insomnia-app/app/network/o-auth-2/{grant-password.js => grant-password.ts} (70%) rename packages/insomnia-app/app/network/o-auth-2/{misc.js => misc.ts} (91%) rename packages/insomnia-app/app/network/o-auth-2/{refresh-token.js => refresh-token.ts} (82%) rename packages/insomnia-app/app/network/{url-matches-cert-host.js => url-matches-cert-host.ts} (88%) rename packages/insomnia-app/app/plugins/context/__tests__/{app.test.js => app.test.ts} (91%) rename packages/insomnia-app/app/plugins/context/__tests__/{data.test.js => data.test.ts} (90%) rename packages/insomnia-app/app/plugins/context/__tests__/{request.test.js => request.test.ts} (84%) rename packages/insomnia-app/app/plugins/context/__tests__/{response.test.js => response.test.ts} (83%) rename packages/insomnia-app/app/plugins/context/__tests__/{store.test.js => store.test.ts} (86%) rename packages/insomnia-app/app/plugins/context/{app.js => app.tsx} (76%) rename packages/insomnia-app/app/plugins/context/{data.js => data.ts} (66%) rename packages/insomnia-app/app/plugins/context/{index.js => index.ts} (98%) delete mode 100644 packages/insomnia-app/app/plugins/context/network.js create mode 100644 packages/insomnia-app/app/plugins/context/network.ts rename packages/insomnia-app/app/plugins/context/{request.js => request.ts} (58%) rename packages/insomnia-app/app/plugins/context/{response.js => response.ts} (70%) delete mode 100644 packages/insomnia-app/app/plugins/context/store.js create mode 100644 packages/insomnia-app/app/plugins/context/store.ts rename packages/insomnia-app/app/plugins/{create.js => create.ts} (96%) rename packages/insomnia-app/app/plugins/{index.js => index.ts} (60%) rename packages/insomnia-app/app/plugins/{install.js => install.ts} (84%) rename packages/insomnia-app/app/plugins/{misc.js => misc.ts} (82%) rename packages/insomnia-app/app/{renderer.js => renderer.ts} (100%) rename packages/insomnia-app/app/sync/delta/__tests__/{diff.test.js => diff.test.ts} (70%) rename packages/insomnia-app/app/sync/delta/__tests__/{patch.test.js => patch.test.ts} (100%) rename packages/insomnia-app/app/sync/delta/{diff.js => diff.ts} (85%) rename packages/insomnia-app/app/sync/delta/{patch.js => patch.ts} (77%) rename packages/insomnia-app/app/sync/git/__mocks__/{path.js => path.ts} (57%) delete mode 100644 packages/insomnia-app/app/sync/git/__mocks__/shallow-clone.js create mode 100644 packages/insomnia-app/app/sync/git/__mocks__/shallow-clone.ts rename packages/insomnia-app/app/sync/git/__tests__/{git-rollback.test.js => git-rollback.test.ts} (79%) rename packages/insomnia-app/app/sync/git/__tests__/{git-vcs.test.js => git-vcs.test.ts} (92%) rename packages/insomnia-app/app/sync/git/__tests__/{mem-client.test.js => mem-client.test.ts} (85%) rename packages/insomnia-app/app/sync/git/__tests__/{ne-db-client.test.js => ne-db-client.test.ts} (77%) rename packages/insomnia-app/app/sync/git/__tests__/{parse-git-path.test.js => parse-git-path.test.ts} (98%) rename packages/insomnia-app/app/sync/git/__tests__/{path-sep.test.js => path-sep.test.ts} (98%) rename packages/insomnia-app/app/sync/git/__tests__/{routable-fs-client.test.js => routable-fs-client.test.ts} (92%) rename packages/insomnia-app/app/sync/git/__tests__/{util.js => util.ts} (100%) rename packages/insomnia-app/app/sync/git/__tests__/{utils.test.js => utils.test.ts} (99%) rename packages/insomnia-app/app/sync/git/{fs-client.js => fs-client.ts} (72%) rename packages/insomnia-app/app/sync/git/{git-rollback.js => git-rollback.ts} (79%) rename packages/insomnia-app/app/sync/git/{git-vcs.js => git-vcs.ts} (74%) rename packages/insomnia-app/app/sync/git/{http-client.js => http-client.ts} (95%) rename packages/insomnia-app/app/sync/git/{mem-client.js => mem-client.ts} (69%) rename packages/insomnia-app/app/sync/git/{ne-db-client.js => ne-db-client.ts} (73%) rename packages/insomnia-app/app/sync/git/{parse-git-path.js => parse-git-path.ts} (89%) rename packages/insomnia-app/app/sync/git/{path-sep.js => path-sep.ts} (98%) rename packages/insomnia-app/app/sync/git/{routable-fs-client.js => routable-fs-client.ts} (84%) rename packages/insomnia-app/app/sync/git/{shallow-clone.js => shallow-clone.ts} (80%) rename packages/insomnia-app/app/sync/git/{stat.js => stat.ts} (80%) create mode 100644 packages/insomnia-app/app/sync/git/system-error.ts rename packages/insomnia-app/app/sync/git/{utils.js => utils.ts} (57%) rename packages/insomnia-app/app/sync/lib/__tests__/{deterministicStringify.test.js => deterministicStringify.test.ts} (86%) rename packages/insomnia-app/app/sync/lib/{deterministicStringify.js => deterministicStringify.ts} (83%) rename packages/insomnia-app/app/sync/store/__tests__/{index.test.js => index.test.ts} (69%) rename packages/insomnia-app/app/sync/store/drivers/{base.js => base.ts} (82%) rename packages/insomnia-app/app/sync/store/drivers/{file-system-driver.js => file-system-driver.ts} (80%) rename packages/insomnia-app/app/sync/store/drivers/{memory-driver.js => memory-driver.ts} (66%) rename packages/insomnia-app/app/sync/store/hooks/__tests__/{compress.test.js => compress.test.ts} (100%) rename packages/insomnia-app/app/sync/store/hooks/{compress.js => compress.ts} (90%) rename packages/insomnia-app/app/sync/store/{index.js => index.ts} (81%) delete mode 100644 packages/insomnia-app/app/sync/types.js create mode 100644 packages/insomnia-app/app/sync/types.ts rename packages/insomnia-app/app/sync/vcs/__tests__/{index.test.js => index.test.ts} (77%) rename packages/insomnia-app/app/sync/vcs/__tests__/{paths.test.js => paths.test.ts} (100%) rename packages/insomnia-app/app/sync/vcs/__tests__/{util.test.js => util.test.ts} (82%) rename packages/insomnia-app/app/sync/vcs/{index.js => index.ts} (84%) delete mode 100644 packages/insomnia-app/app/sync/vcs/paths.js create mode 100644 packages/insomnia-app/app/sync/vcs/paths.ts rename packages/insomnia-app/app/sync/vcs/{util.js => util.ts} (86%) rename packages/insomnia-app/app/templating/__tests__/{utils.test.js => utils.test.ts} (58%) rename packages/insomnia-app/app/templating/{base-extension.js => base-extension.ts} (72%) delete mode 100644 packages/insomnia-app/app/templating/extensions/index.js create mode 100644 packages/insomnia-app/app/templating/extensions/index.ts rename packages/insomnia-app/app/templating/{index.js => index.ts} (83%) rename packages/insomnia-app/app/templating/{utils.js => utils.ts} (69%) rename packages/insomnia-app/app/{test-utils.js => test-utils.ts} (68%) rename packages/insomnia-app/app/ui/components/{activity-toggle.js => activity-toggle.tsx} (51%) rename packages/insomnia-app/app/ui/components/{analytics.js => analytics.tsx} (76%) rename packages/insomnia-app/app/ui/components/base/__tests__/{editable.test.js => editable.test.ts} (100%) rename packages/insomnia-app/app/ui/components/base/{button.js => button.tsx} (51%) rename packages/insomnia-app/app/ui/components/base/{copy-button.js => copy-button.tsx} (63%) rename packages/insomnia-app/app/ui/components/base/{debounced-input.js => debounced-input.tsx} (60%) rename packages/insomnia-app/app/ui/components/base/dropdown/{dropdown-button.js => dropdown-button.tsx} (50%) rename packages/insomnia-app/app/ui/components/base/dropdown/{dropdown-divider.js => dropdown-divider.tsx} (66%) rename packages/insomnia-app/app/ui/components/base/dropdown/{dropdown-hint.js => dropdown-hint.tsx} (64%) rename packages/insomnia-app/app/ui/components/base/dropdown/{dropdown-item.js => dropdown-item.tsx} (55%) rename packages/insomnia-app/app/ui/components/base/dropdown/{dropdown-right.js => dropdown-right.tsx} (57%) rename packages/insomnia-app/app/ui/components/base/dropdown/{dropdown.js => dropdown.tsx} (77%) rename packages/insomnia-app/app/ui/components/base/dropdown/{index.js => index.ts} (100%) rename packages/insomnia-app/app/ui/components/base/{editable.stories.js => editable.stories.tsx} (84%) rename packages/insomnia-app/app/ui/components/base/{editable.js => editable.tsx} (77%) rename packages/insomnia-app/app/ui/components/base/{file-input-button.js => file-input-button.tsx} (68%) rename packages/insomnia-app/app/ui/components/base/{highlight.js => highlight.tsx} (69%) rename packages/insomnia-app/app/ui/components/base/{indeterminate-checkbox.js => indeterminate-checkbox.tsx} (56%) rename packages/insomnia-app/app/ui/components/base/{lazy.js => lazy.ts} (65%) rename packages/insomnia-app/app/ui/components/base/{link.js => link.tsx} (53%) rename packages/insomnia-app/app/ui/components/base/{mailto.js => mailto.tsx} (59%) rename packages/insomnia-app/app/ui/components/base/{modal-body.js => modal-body.tsx} (60%) rename packages/insomnia-app/app/ui/components/base/{modal-footer.js => modal-footer.tsx} (50%) rename packages/insomnia-app/app/ui/components/base/{modal-header.js => modal-header.tsx} (64%) rename packages/insomnia-app/app/ui/components/base/{modal.js => modal.tsx} (76%) rename packages/insomnia-app/app/ui/components/base/{prompt-button.stories.js => prompt-button.stories.tsx} (82%) rename packages/insomnia-app/app/ui/components/base/{prompt-button.js => prompt-button.tsx} (53%) rename packages/insomnia-app/app/ui/components/buttons/__tests__/{grpc-send-button.test.js => grpc-send-button.test.tsx} (99%) rename packages/insomnia-app/app/ui/components/buttons/{grpc-send-button.js => grpc-send-button.tsx} (62%) rename packages/insomnia-app/app/ui/components/buttons/{settings-button.js => settings-button.tsx} (67%) rename packages/insomnia-app/app/ui/components/{check-for-updates-button.js => check-for-updates-button.tsx} (58%) rename packages/insomnia-app/app/ui/components/codemirror/{base-imports.js => base-imports.ts} (97%) rename packages/insomnia-app/app/ui/components/codemirror/{code-editor.js => code-editor.tsx} (78%) rename packages/insomnia-app/app/ui/components/codemirror/extensions/{autocomplete.js => autocomplete.ts} (90%) rename packages/insomnia-app/app/ui/components/codemirror/extensions/{clickable.js => clickable.ts} (81%) rename packages/insomnia-app/app/ui/components/codemirror/extensions/{nunjucks-tags.js => nunjucks-tags.ts} (73%) rename packages/insomnia-app/app/ui/components/codemirror/lint/{javascript-async-lint.js => javascript-async-lint.ts} (98%) rename packages/insomnia-app/app/ui/components/codemirror/lint/{openapi.js => openapi.ts} (85%) rename packages/insomnia-app/app/ui/components/codemirror/modes/{curl.js => curl.ts} (80%) rename packages/insomnia-app/app/ui/components/codemirror/modes/{nunjucks.js => nunjucks.ts} (97%) rename packages/insomnia-app/app/ui/components/codemirror/modes/{openapi.js => openapi.ts} (99%) rename packages/insomnia-app/app/ui/components/codemirror/{one-line-editor.js => one-line-editor.tsx} (72%) rename packages/insomnia-app/app/ui/components/{cookie-list.js => cookie-list.tsx} (76%) rename packages/insomnia-app/app/ui/components/dropdowns/{account-dropdown.js => account-dropdown.tsx} (82%) rename packages/insomnia-app/app/ui/components/dropdowns/{auth-dropdown.js => auth-dropdown.tsx} (88%) rename packages/insomnia-app/app/ui/components/dropdowns/{content-type-dropdown.js => content-type-dropdown.tsx} (90%) rename packages/insomnia-app/app/ui/components/dropdowns/{document-card-dropdown.js => document-card-dropdown.tsx} (79%) rename packages/insomnia-app/app/ui/components/dropdowns/{environments-dropdown.js => environments-dropdown.tsx} (81%) rename packages/insomnia-app/app/ui/components/dropdowns/{git-sync-dropdown.js => git-sync-dropdown.tsx} (78%) rename packages/insomnia-app/app/ui/components/dropdowns/grpc-method-dropdown/__tests__/{grpc-method-dropdown-button.test.js => grpc-method-dropdown-button.test.tsx} (99%) rename packages/insomnia-app/app/ui/components/dropdowns/grpc-method-dropdown/__tests__/{grpc-method-dropdown.test.js => grpc-method-dropdown.test.tsx} (99%) rename packages/insomnia-app/app/ui/components/dropdowns/grpc-method-dropdown/{grpc-method-dropdown-button.js => grpc-method-dropdown-button.tsx} (78%) rename packages/insomnia-app/app/ui/components/dropdowns/grpc-method-dropdown/{grpc-method-dropdown.js => grpc-method-dropdown.tsx} (74%) rename packages/insomnia-app/app/ui/components/dropdowns/grpc-method-dropdown/{index.js => index.ts} (100%) rename packages/insomnia-app/app/ui/components/dropdowns/{method-dropdown.js => method-dropdown.tsx} (83%) rename packages/insomnia-app/app/ui/components/dropdowns/{preview-mode-dropdown.js => preview-mode-dropdown.tsx} (86%) rename packages/insomnia-app/app/ui/components/dropdowns/{remote-workspaces-dropdown.js => remote-workspaces-dropdown.tsx} (83%) rename packages/insomnia-app/app/ui/components/dropdowns/{request-actions-dropdown.js => request-actions-dropdown.tsx} (68%) rename packages/insomnia-app/app/ui/components/dropdowns/{request-group-actions-dropdown.js => request-group-actions-dropdown.tsx} (74%) rename packages/insomnia-app/app/ui/components/dropdowns/{response-history-dropdown.js => response-history-dropdown.tsx} (77%) rename packages/insomnia-app/app/ui/components/dropdowns/{sync-dropdown.js => sync-dropdown.tsx} (85%) rename packages/insomnia-app/app/ui/components/dropdowns/{workspace-dropdown.js => workspace-dropdown.tsx} (72%) rename packages/insomnia-app/app/ui/components/editors/__tests__/{environment-editor.test.js => environment-editor.test.ts} (99%) rename packages/insomnia-app/app/ui/components/editors/__tests__/{password-editor.test.js => password-editor.test.tsx} (99%) rename packages/insomnia-app/app/ui/components/editors/auth/{asap-auth.js => asap-auth.tsx} (89%) rename packages/insomnia-app/app/ui/components/editors/auth/{auth-wrapper.js => auth-wrapper.tsx} (89%) rename packages/insomnia-app/app/ui/components/editors/auth/{aws-auth.js => aws-auth.tsx} (86%) rename packages/insomnia-app/app/ui/components/editors/auth/{basic-auth.js => basic-auth.tsx} (89%) rename packages/insomnia-app/app/ui/components/editors/auth/{bearer-auth.js => bearer-auth.tsx} (89%) rename packages/insomnia-app/app/ui/components/editors/auth/{digest-auth.js => digest-auth.tsx} (86%) rename packages/insomnia-app/app/ui/components/editors/auth/{hawk-auth.js => hawk-auth.tsx} (82%) rename packages/insomnia-app/app/ui/components/editors/auth/{netrc-auth.js => netrc-auth.tsx} (100%) rename packages/insomnia-app/app/ui/components/editors/auth/{ntlm-auth.js => ntlm-auth.tsx} (86%) rename packages/insomnia-app/app/ui/components/editors/auth/{o-auth-1-auth.js => o-auth-1-auth.tsx} (84%) rename packages/insomnia-app/app/ui/components/editors/auth/{o-auth-2-auth.js => o-auth-2-auth.tsx} (82%) rename packages/insomnia-app/app/ui/components/editors/body/{body-editor.js => body-editor.tsx} (86%) rename packages/insomnia-app/app/ui/components/editors/body/{file-editor.js => file-editor.tsx} (90%) rename packages/insomnia-app/app/ui/components/editors/body/{form-editor.js => form-editor.tsx} (73%) rename packages/insomnia-app/app/ui/components/editors/body/{graph-ql-editor.js => graph-ql-editor.tsx} (81%) rename packages/insomnia-app/app/ui/components/editors/body/{raw-editor.js => raw-editor.tsx} (57%) rename packages/insomnia-app/app/ui/components/editors/body/{url-encoded-editor.js => url-encoded-editor.tsx} (72%) rename packages/insomnia-app/app/ui/components/editors/{environment-editor.js => environment-editor.tsx} (77%) rename packages/insomnia-app/app/ui/components/editors/{grpc-editor.js => grpc-editor.tsx} (66%) rename packages/insomnia-app/app/ui/components/editors/{password-editor.js => password-editor.tsx} (71%) rename packages/insomnia-app/app/ui/components/editors/{request-headers-editor.js => request-headers-editor.tsx} (85%) rename packages/insomnia-app/app/ui/components/editors/{request-parameters-editor.js => request-parameters-editor.tsx} (81%) rename packages/insomnia-app/app/ui/components/{error-boundary.js => error-boundary.tsx} (66%) rename packages/insomnia-app/app/ui/components/export-requests/{request-group-row.js => request-group-row.tsx} (79%) rename packages/insomnia-app/app/ui/components/export-requests/{request-row.js => request-row.tsx} (76%) rename packages/insomnia-app/app/ui/components/export-requests/{tree.js => tree.tsx} (79%) rename packages/insomnia-app/app/ui/components/{forms.stories.js => forms.stories.tsx} (98%) rename packages/insomnia-app/app/ui/components/graph-ql-explorer/{graph-ql-default-value.js => graph-ql-default-value.tsx} (71%) rename packages/insomnia-app/app/ui/components/graph-ql-explorer/{graph-ql-explorer-enum.js => graph-ql-explorer-enum.tsx} (88%) rename packages/insomnia-app/app/ui/components/graph-ql-explorer/{graph-ql-explorer-field-link.js => graph-ql-explorer-field-link.tsx} (62%) rename packages/insomnia-app/app/ui/components/graph-ql-explorer/{graph-ql-explorer-field.js => graph-ql-explorer-field.tsx} (78%) rename packages/insomnia-app/app/ui/components/graph-ql-explorer/{graph-ql-explorer-schema.js => graph-ql-explorer-schema.tsx} (82%) rename packages/insomnia-app/app/ui/components/graph-ql-explorer/{graph-ql-explorer-type-link.js => graph-ql-explorer-type-link.tsx} (63%) rename packages/insomnia-app/app/ui/components/graph-ql-explorer/{graph-ql-explorer-type.js => graph-ql-explorer-type.tsx} (81%) rename packages/insomnia-app/app/ui/components/graph-ql-explorer/{graph-ql-explorer.js => graph-ql-explorer.tsx} (77%) delete mode 100644 packages/insomnia-app/app/ui/components/grpc-spinner.js create mode 100644 packages/insomnia-app/app/ui/components/grpc-spinner.tsx delete mode 100644 packages/insomnia-app/app/ui/components/help-tooltip.js rename packages/insomnia-app/app/ui/components/{help-tooltip.stories.js => help-tooltip.stories.tsx} (84%) create mode 100644 packages/insomnia-app/app/ui/components/help-tooltip.tsx rename packages/insomnia-app/app/ui/components/{hotkey.js => hotkey.tsx} (73%) rename packages/insomnia-app/app/ui/components/{html-element-wrapper.js => html-element-wrapper.tsx} (77%) rename packages/insomnia-app/app/ui/components/key-value-editor/{editor.js => editor.tsx} (85%) rename packages/insomnia-app/app/ui/components/key-value-editor/{row.js => row.tsx} (71%) rename packages/insomnia-app/app/ui/components/{keydown-binder.js => keydown-binder.ts} (85%) rename packages/insomnia-app/app/ui/components/{markdown-editor.js => markdown-editor.tsx} (77%) rename packages/insomnia-app/app/ui/components/{markdown-preview.js => markdown-preview.tsx} (71%) rename packages/insomnia-app/app/ui/components/modals/__mocks__/{index.js => index.ts} (67%) rename packages/insomnia-app/app/ui/components/modals/{add-key-combination-modal.js => add-key-combination-modal.tsx} (85%) rename packages/insomnia-app/app/ui/components/modals/{alert-modal.js => alert-modal.tsx} (70%) rename packages/insomnia-app/app/ui/components/modals/{ask-modal.js => ask-modal.tsx} (73%) rename packages/insomnia-app/app/ui/components/modals/{code-prompt-modal.js => code-prompt-modal.tsx} (78%) rename packages/insomnia-app/app/ui/components/modals/{cookie-modify-modal.js => cookie-modify-modal.tsx} (83%) rename packages/insomnia-app/app/ui/components/modals/{cookies-modal.js => cookies-modal.tsx} (80%) rename packages/insomnia-app/app/ui/components/modals/{environment-edit-modal.js => environment-edit-modal.tsx} (75%) rename packages/insomnia-app/app/ui/components/modals/{error-modal.js => error-modal.tsx} (79%) rename packages/insomnia-app/app/ui/components/modals/{export-requests-modal.js => export-requests-modal.tsx} (86%) rename packages/insomnia-app/app/ui/components/modals/{filter-help-modal.js => filter-help-modal.tsx} (91%) rename packages/insomnia-app/app/ui/components/modals/{generate-code-modal.js => generate-code-modal.tsx} (78%) rename packages/insomnia-app/app/ui/components/modals/{generate-config-modal.js => generate-config-modal.tsx} (81%) rename packages/insomnia-app/app/ui/components/modals/{git-branches-modal.js => git-branches-modal.tsx} (83%) rename packages/insomnia-app/app/ui/components/modals/{git-log-modal.js => git-log-modal.tsx} (84%) rename packages/insomnia-app/app/ui/components/modals/{git-repository-settings-modal.js => git-repository-settings-modal.tsx} (84%) rename packages/insomnia-app/app/ui/components/modals/{git-staging-modal.js => git-staging-modal.tsx} (85%) rename packages/insomnia-app/app/ui/components/modals/{index.js => index.ts} (99%) rename packages/insomnia-app/app/ui/components/modals/{login-modal.js => login-modal.tsx} (71%) rename packages/insomnia-app/app/ui/components/modals/{move-request-group-modal.js => move-request-group-modal.tsx} (77%) rename packages/insomnia-app/app/ui/components/modals/{nunjucks-modal.js => nunjucks-modal.tsx} (73%) rename packages/insomnia-app/app/ui/components/modals/{payment-notification-modal.js => payment-notification-modal.tsx} (89%) rename packages/insomnia-app/app/ui/components/modals/{prompt-modal.js => prompt-modal.tsx} (70%) rename packages/insomnia-app/app/ui/components/modals/{proto-files-modal.js => proto-files-modal.tsx} (74%) rename packages/insomnia-app/app/ui/components/modals/{request-create-modal.js => request-create-modal.tsx} (74%) rename packages/insomnia-app/app/ui/components/modals/{request-render-error-modal.js => request-render-error-modal.tsx} (86%) rename packages/insomnia-app/app/ui/components/modals/{request-settings-modal.js => request-settings-modal.tsx} (81%) rename packages/insomnia-app/app/ui/components/modals/{request-switcher-modal.js => request-switcher-modal.tsx} (84%) rename packages/insomnia-app/app/ui/components/modals/{response-debug-modal.js => response-debug-modal.tsx} (73%) rename packages/insomnia-app/app/ui/components/modals/{select-modal.js => select-modal.tsx} (66%) rename packages/insomnia-app/app/ui/components/modals/{settings-modal.js => settings-modal.tsx} (83%) rename packages/insomnia-app/app/ui/components/modals/{sync-branches-modal.js => sync-branches-modal.tsx} (80%) rename packages/insomnia-app/app/ui/components/modals/{sync-delete-modal.js => sync-delete-modal.tsx} (74%) rename packages/insomnia-app/app/ui/components/modals/{sync-history-modal.js => sync-history-modal.tsx} (84%) rename packages/insomnia-app/app/ui/components/modals/{sync-merge-modal.js => sync-merge-modal.tsx} (79%) rename packages/insomnia-app/app/ui/components/modals/{sync-share-modal.js => sync-share-modal.tsx} (83%) rename packages/insomnia-app/app/ui/components/modals/{sync-staging-modal.js => sync-staging-modal.tsx} (82%) rename packages/insomnia-app/app/ui/components/modals/{workspace-environments-edit-modal.js => workspace-environments-edit-modal.tsx} (81%) rename packages/insomnia-app/app/ui/components/modals/{workspace-settings-modal.js => workspace-settings-modal.tsx} (86%) rename packages/insomnia-app/app/ui/components/modals/{wrapper-modal.js => wrapper-modal.tsx} (52%) rename packages/insomnia-app/app/ui/components/{notice.stories.js => notice.stories.tsx} (95%) rename packages/insomnia-app/app/ui/components/{notice.js => notice.tsx} (78%) rename packages/insomnia-app/app/ui/components/{onboarding-container.js => onboarding-container.tsx} (75%) rename packages/insomnia-app/app/ui/components/{page-layout.js => page-layout.tsx} (78%) rename packages/insomnia-app/app/ui/components/panes/{blank-pane.js => blank-pane.tsx} (50%) rename packages/insomnia-app/app/ui/components/panes/grpc-request-pane/__tests__/{use-proto-file-reload.test.js => use-proto-file-reload.test.ts} (84%) rename packages/insomnia-app/app/ui/components/panes/grpc-request-pane/{index.js => index.tsx} (87%) rename packages/insomnia-app/app/ui/components/panes/grpc-request-pane/{use-action-handlers.js => use-action-handlers.ts} (87%) rename packages/insomnia-app/app/ui/components/panes/grpc-request-pane/{use-change-handlers.js => use-change-handlers.ts} (69%) rename packages/insomnia-app/app/ui/components/panes/grpc-request-pane/{use-existing-grpc-urls.js => use-existing-grpc-urls.ts} (98%) rename packages/insomnia-app/app/ui/components/panes/grpc-request-pane/{use-proto-file-reload.js => use-proto-file-reload.ts} (89%) rename packages/insomnia-app/app/ui/components/panes/grpc-request-pane/{use-selected-method.js => use-selected-method.ts} (70%) rename packages/insomnia-app/app/ui/components/panes/{grpc-response-pane.js => grpc-response-pane.tsx} (81%) rename packages/insomnia-app/app/ui/components/panes/{pane.js => pane.tsx} (51%) rename packages/insomnia-app/app/ui/components/panes/{placeholder-request-pane.js => placeholder-request-pane.tsx} (85%) rename packages/insomnia-app/app/ui/components/panes/{placeholder-response-pane.js => placeholder-response-pane.tsx} (90%) rename packages/insomnia-app/app/ui/components/panes/{request-pane.js => request-pane.tsx} (87%) rename packages/insomnia-app/app/ui/components/panes/{response-pane.js => response-pane.tsx} (89%) rename packages/insomnia-app/app/ui/components/proto-file/{proto-directory-list-item.js => proto-directory-list-item.tsx} (69%) rename packages/insomnia-app/app/ui/components/proto-file/{proto-file-list-item.js => proto-file-list-item.tsx} (75%) rename packages/insomnia-app/app/ui/components/proto-file/{proto-file-list.js => proto-file-list.tsx} (79%) rename packages/insomnia-app/app/ui/components/proto-file/{proto-list-item.js => proto-list-item.ts} (68%) rename packages/insomnia-app/app/ui/components/{rendered-query-string.js => rendered-query-string.tsx} (64%) delete mode 100644 packages/insomnia-app/app/ui/components/rendered-text.js create mode 100644 packages/insomnia-app/app/ui/components/rendered-text.tsx rename packages/insomnia-app/app/ui/components/{request-url-bar.js => request-url-bar.tsx} (79%) rename packages/insomnia-app/app/ui/components/{response-timer.js => response-timer.tsx} (72%) rename packages/insomnia-app/app/ui/components/settings/{account.js => account.tsx} (80%) rename packages/insomnia-app/app/ui/components/settings/{general.js => general.tsx} (85%) rename packages/insomnia-app/app/ui/components/settings/{import-export.js => import-export.tsx} (87%) rename packages/insomnia-app/app/ui/components/settings/{plugins.js => plugins.tsx} (82%) rename packages/insomnia-app/app/ui/components/settings/{shortcuts.js => shortcuts.tsx} (84%) rename packages/insomnia-app/app/ui/components/settings/{theme.js => theme.tsx} (87%) rename packages/insomnia-app/app/ui/components/sidebar/{sidebar-children.js => sidebar-children.tsx} (73%) rename packages/insomnia-app/app/ui/components/sidebar/{sidebar-create-dropdown.js => sidebar-create-dropdown.tsx} (68%) rename packages/insomnia-app/app/ui/components/sidebar/{sidebar-filter.js => sidebar-filter.tsx} (78%) rename packages/insomnia-app/app/ui/components/sidebar/{sidebar-request-group-row.js => sidebar-request-group-row.tsx} (78%) rename packages/insomnia-app/app/ui/components/sidebar/{sidebar-request-row.js => sidebar-request-row.tsx} (72%) delete mode 100644 packages/insomnia-app/app/ui/components/sidebar/sidebar-sort-dropdown.js create mode 100644 packages/insomnia-app/app/ui/components/sidebar/sidebar-sort-dropdown.tsx rename packages/insomnia-app/app/ui/components/sidebar/{sidebar.js => sidebar.tsx} (66%) rename packages/insomnia-app/app/ui/components/spec-editor/{spec-editor-sidebar.js => spec-editor-sidebar.tsx} (76%) rename packages/insomnia-app/app/ui/components/{sync-pull-button.js => sync-pull-button.tsx} (74%) rename packages/insomnia-app/app/ui/components/tags/{grpc-method-tag.js => grpc-method-tag.tsx} (75%) delete mode 100644 packages/insomnia-app/app/ui/components/tags/grpc-status-tag.js create mode 100644 packages/insomnia-app/app/ui/components/tags/grpc-status-tag.tsx rename packages/insomnia-app/app/ui/components/tags/{grpc-tag.js => grpc-tag.tsx} (78%) rename packages/insomnia-app/app/ui/components/tags/{method-tag.js => method-tag.tsx} (75%) rename packages/insomnia-app/app/ui/components/tags/{size-tag.js => size-tag.tsx} (75%) rename packages/insomnia-app/app/ui/components/tags/{status-tag.js => status-tag.tsx} (77%) rename packages/insomnia-app/app/ui/components/tags/{time-tag.js => time-tag.tsx} (72%) rename packages/insomnia-app/app/ui/components/tags/{url-tag.js => url-tag.tsx} (57%) rename packages/insomnia-app/app/ui/components/templating/{tag-editor.js => tag-editor.tsx} (88%) rename packages/insomnia-app/app/ui/components/templating/{variable-editor.js => variable-editor.tsx} (77%) rename packages/insomnia-app/app/ui/components/{time-from-now.js => time-from-now.tsx} (69%) rename packages/insomnia-app/app/ui/components/{toast.js => toast.tsx} (78%) rename packages/insomnia-app/app/ui/components/{tooltip.stories.js => tooltip.stories.tsx} (91%) rename packages/insomnia-app/app/ui/components/{tooltip.js => tooltip.tsx} (69%) delete mode 100644 packages/insomnia-app/app/ui/components/unit-test-editable.js create mode 100644 packages/insomnia-app/app/ui/components/unit-test-editable.tsx rename packages/insomnia-app/app/ui/components/viewers/{grpc-tabbed-messages.js => grpc-tabbed-messages.tsx} (76%) rename packages/insomnia-app/app/ui/components/viewers/{response-cookies-viewer.js => response-cookies-viewer.tsx} (81%) rename packages/insomnia-app/app/ui/components/viewers/{response-csv-viewer.js => response-csv-viewer.tsx} (71%) rename packages/insomnia-app/app/ui/components/viewers/{response-error.js => response-error.tsx} (79%) rename packages/insomnia-app/app/ui/components/viewers/{response-headers-viewer.js => response-headers-viewer.tsx} (69%) rename packages/insomnia-app/app/ui/components/viewers/{response-multipart.js => response-multipart.tsx} (80%) rename packages/insomnia-app/app/ui/components/viewers/{response-pdf-viewer.js => response-pdf-viewer.tsx} (77%) rename packages/insomnia-app/app/ui/components/viewers/{response-raw.js => response-raw.tsx} (83%) rename packages/insomnia-app/app/ui/components/viewers/{response-timeline-viewer.js => response-timeline-viewer.tsx} (86%) rename packages/insomnia-app/app/ui/components/viewers/{response-viewer.js => response-viewer.tsx} (87%) rename packages/insomnia-app/app/ui/components/viewers/{response-web-view.js => response-web-view.tsx} (76%) rename packages/insomnia-app/app/ui/components/{workspace-page-header.js => workspace-page-header.tsx} (79%) rename packages/insomnia-app/app/ui/components/{wrapper-analytics.js => wrapper-analytics.tsx} (76%) rename packages/insomnia-app/app/ui/components/{wrapper-debug.js => wrapper-debug.tsx} (82%) rename packages/insomnia-app/app/ui/components/{wrapper-design.js => wrapper-design.tsx} (89%) rename packages/insomnia-app/app/ui/components/{wrapper-home.js => wrapper-home.tsx} (80%) rename packages/insomnia-app/app/ui/components/{wrapper-migration.js => wrapper-migration.tsx} (77%) rename packages/insomnia-app/app/ui/components/{wrapper-onboarding.js => wrapper-onboarding.tsx} (86%) rename packages/insomnia-app/app/ui/components/{wrapper-unit-test.js => wrapper-unit-test.tsx} (82%) rename packages/insomnia-app/app/ui/components/{wrapper.js => wrapper.tsx} (79%) rename packages/insomnia-app/app/ui/containers/{app.js => app.tsx} (75%) rename packages/insomnia-app/app/ui/context/app/{drag-drop-context.js => drag-drop-context.ts} (78%) rename packages/insomnia-app/app/ui/context/grpc/__mocks__/{grpc-actions.js => grpc-actions.ts} (73%) rename packages/insomnia-app/app/ui/context/grpc/__schemas__/{grpc-message-schema.js => grpc-message-schema.ts} (86%) rename packages/insomnia-app/app/ui/context/grpc/__schemas__/{grpc-method-definition-schema.js => grpc-method-definition-schema.ts} (89%) delete mode 100644 packages/insomnia-app/app/ui/context/grpc/__schemas__/grpc-status-object-schema.js create mode 100644 packages/insomnia-app/app/ui/context/grpc/__schemas__/grpc-status-object-schema.ts rename packages/insomnia-app/app/ui/context/grpc/__schemas__/{index.js => index.ts} (100%) rename packages/insomnia-app/app/ui/context/grpc/__schemas__/{request-state-schema.js => request-state-schema.ts} (97%) rename packages/insomnia-app/app/ui/context/grpc/__tests__/{grpc-context.test.js => grpc-context.test.tsx} (97%) rename packages/insomnia-app/app/ui/context/grpc/__tests__/{grpc-ipc-renderer.test.js => grpc-ipc-renderer.test.ts} (98%) rename packages/insomnia-app/app/ui/context/grpc/__tests__/{grpc-reducer.test.js => grpc-reducer.test.ts} (83%) rename packages/insomnia-app/app/ui/context/grpc/{grpc-actions.js => grpc-actions.ts} (58%) rename packages/insomnia-app/app/ui/context/grpc/{grpc-context.js => grpc-context.tsx} (67%) rename packages/insomnia-app/app/ui/context/grpc/{grpc-ipc-renderer.js => grpc-ipc-renderer.ts} (99%) rename packages/insomnia-app/app/ui/context/grpc/{grpc-reducer.js => grpc-reducer.ts} (77%) rename packages/insomnia-app/app/ui/context/grpc/{index.js => index.ts} (97%) delete mode 100644 packages/insomnia-app/app/ui/dnd-backend.js create mode 100644 packages/insomnia-app/app/ui/dnd-backend.ts rename packages/insomnia-app/app/ui/{index.js => index.tsx} (90%) rename packages/insomnia-app/app/ui/redux/__tests__/{proto-selectors.test.js => proto-selectors.test.ts} (68%) rename packages/insomnia-app/app/ui/redux/__tests__/{sidebar-selectors.test.js => sidebar-selectors.test.ts} (66%) rename packages/insomnia-app/app/ui/redux/{create.js => create.ts} (74%) rename packages/insomnia-app/app/ui/redux/modules/__tests__/{git.test.js => git.test.tsx} (80%) rename packages/insomnia-app/app/ui/redux/modules/__tests__/{global.test.js => global.test.ts} (70%) rename packages/insomnia-app/app/ui/redux/modules/__tests__/{helpers.test.js => helpers.test.ts} (98%) rename packages/insomnia-app/app/ui/redux/modules/__tests__/{workspace.test.js => workspace.test.ts} (80%) rename packages/insomnia-app/app/ui/redux/modules/{entities.js => entities.ts} (94%) rename packages/insomnia-app/app/ui/redux/modules/{git.js => git.tsx} (88%) rename packages/insomnia-app/app/ui/redux/modules/{global.js => global.tsx} (86%) rename packages/insomnia-app/app/ui/redux/modules/{helpers.js => helpers.ts} (72%) rename packages/insomnia-app/app/ui/redux/modules/{index.js => index.ts} (90%) rename packages/insomnia-app/app/ui/redux/modules/{workspace.js => workspace.ts} (73%) rename packages/insomnia-app/app/ui/redux/{proto-selectors.js => proto-selectors.ts} (77%) rename packages/insomnia-app/app/ui/redux/{selectors.js => selectors.ts} (88%) rename packages/insomnia-app/app/ui/redux/{sidebar-selectors.js => sidebar-selectors.ts} (94%) delete mode 100644 packages/insomnia-app/config/index.js delete mode 100644 packages/insomnia-app/flow-typed/anybase.js delete mode 100644 packages/insomnia-app/flow-typed/autobind-decorator.js delete mode 100644 packages/insomnia-app/flow-typed/aws4.js delete mode 100644 packages/insomnia-app/flow-typed/classnames.js delete mode 100644 packages/insomnia-app/flow-typed/clone.js delete mode 100644 packages/insomnia-app/flow-typed/codemirror.js delete mode 100644 packages/insomnia-app/flow-typed/color.js delete mode 100644 packages/insomnia-app/flow-typed/deep-equal.js delete mode 100644 packages/insomnia-app/flow-typed/electron-context-menu.js delete mode 100644 packages/insomnia-app/flow-typed/electron-squirrel-startup.js delete mode 100644 packages/insomnia-app/flow-typed/electron-updater.js delete mode 100644 packages/insomnia-app/flow-typed/electron.js delete mode 100644 packages/insomnia-app/flow-typed/events.js delete mode 100644 packages/insomnia-app/flow-typed/font-scanner.js delete mode 100644 packages/insomnia-app/flow-typed/fs-extra.js delete mode 100644 packages/insomnia-app/flow-typed/fuzzysort.js delete mode 100644 packages/insomnia-app/flow-typed/hawk.js delete mode 100644 packages/insomnia-app/flow-typed/iconv-lite.js delete mode 100644 packages/insomnia-app/flow-typed/insomnia-cookies.js delete mode 100644 packages/insomnia-app/flow-typed/insomnia-importers.js delete mode 100644 packages/insomnia-app/flow-typed/insomnia-plugin-hash.js delete mode 100644 packages/insomnia-app/flow-typed/insomnia-prettify.js delete mode 100644 packages/insomnia-app/flow-typed/insomnia-url.js delete mode 100644 packages/insomnia-app/flow-typed/isomorphic-git.js delete mode 100644 packages/insomnia-app/flow-typed/jest.js delete mode 100644 packages/insomnia-app/flow-typed/json-order.js delete mode 100644 packages/insomnia-app/flow-typed/jsonpath.js delete mode 100644 packages/insomnia-app/flow-typed/jwt-authentication.js delete mode 100644 packages/insomnia-app/flow-typed/mime-types.js delete mode 100644 packages/insomnia-app/flow-typed/mkdirp.js delete mode 100644 packages/insomnia-app/flow-typed/moment.js delete mode 100644 packages/insomnia-app/flow-typed/multiparty.js delete mode 100644 packages/insomnia-app/flow-typed/nedb.js delete mode 100644 packages/insomnia-app/flow-typed/node-libcurl.js delete mode 100644 packages/insomnia-app/flow-typed/nunjucks.js delete mode 100644 packages/insomnia-app/flow-typed/oauth-1.0a.js delete mode 100644 packages/insomnia-app/flow-typed/objectpath.js delete mode 100644 packages/insomnia-app/flow-typed/openapi-2-kong.js delete mode 100644 packages/insomnia-app/flow-typed/papaparse.js delete mode 100644 packages/insomnia-app/flow-typed/pdfjs-dist.js delete mode 100644 packages/insomnia-app/flow-typed/react-dom.js delete mode 100644 packages/insomnia-app/flow-typed/react-sortable-hoc.js delete mode 100644 packages/insomnia-app/flow-typed/react-tabs.js delete mode 100644 packages/insomnia-app/flow-typed/reselect.js delete mode 100644 packages/insomnia-app/flow-typed/rimraf.js delete mode 100644 packages/insomnia-app/flow-typed/styled-components.js delete mode 100644 packages/insomnia-app/flow-typed/swagger-ui-react.js delete mode 100644 packages/insomnia-app/flow-typed/tough-cookie.js delete mode 100644 packages/insomnia-app/flow-typed/url-join.js delete mode 100644 packages/insomnia-app/flow-typed/uuid.js delete mode 100644 packages/insomnia-app/flow-typed/xmldom.js delete mode 100644 packages/insomnia-app/flow-typed/yaml.js create mode 100644 packages/insomnia-app/jest.config.js delete mode 100644 packages/insomnia-app/scripts/build.js create mode 100644 packages/insomnia-app/scripts/build.ts rename packages/insomnia-app/scripts/{getBuildContext.js => getBuildContext.ts} (64%) delete mode 100644 packages/insomnia-app/scripts/package.js create mode 100644 packages/insomnia-app/scripts/package.ts delete mode 100644 packages/insomnia-app/scripts/release.js create mode 100644 packages/insomnia-app/scripts/release.ts rename packages/{insomnia-inso/src/__fixtures__/.insorc-blank.yaml => insomnia-app/scripts/startDevServer.ts} (100%) rename packages/insomnia-app/send-request/{index.js => index.ts} (100%) create mode 100644 packages/insomnia-app/tsconfig.build.json create mode 100644 packages/insomnia-app/tsconfig.build.sr.json create mode 100644 packages/insomnia-app/tsconfig.json create mode 100644 packages/insomnia-app/tsconfig.webpack.json rename packages/insomnia-app/webpack/{webpack.config.base.babel.js => webpack.config.base.ts} (69%) delete mode 100644 packages/insomnia-app/webpack/webpack.config.development.babel.js create mode 100644 packages/insomnia-app/webpack/webpack.config.development.ts rename packages/insomnia-app/webpack/{webpack.config.electron.babel.js => webpack.config.electron.ts} (59%) rename packages/insomnia-app/webpack/{webpack.config.production.babel.js => webpack.config.production.ts} (61%) rename packages/insomnia-app/{send-request/webpack.config.babel.js => webpack/webpack.config.sr.ts} (58%) delete mode 100644 packages/insomnia-components/.babelrc create mode 100644 packages/insomnia-components/.eslintignore create mode 100644 packages/insomnia-components/.eslintrc.js delete mode 100644 packages/insomnia-components/.flowconfig delete mode 100644 packages/insomnia-components/.svgrrc.json delete mode 100644 packages/insomnia-components/__mocks__/dummy.js delete mode 100644 packages/insomnia-components/components/breadcrumb.stories.js delete mode 100644 packages/insomnia-components/components/button/async-button.js delete mode 100644 packages/insomnia-components/components/button/index.js delete mode 100644 packages/insomnia-components/components/card-container.js delete mode 100644 packages/insomnia-components/components/header.stories.js delete mode 100644 packages/insomnia-components/components/list-group/unit-test-result-badge.js delete mode 100644 packages/insomnia-components/components/list-group/unit-test-result-item.js delete mode 100644 packages/insomnia-components/components/multi-switch.stories.js delete mode 100644 packages/insomnia-components/components/radio-button-group.stories.js delete mode 100644 packages/insomnia-components/components/sidebar/sidebar-info.js delete mode 100644 packages/insomnia-components/components/sidebar/sidebar-invalid-section.js delete mode 100644 packages/insomnia-components/components/sidebar/sidebar-panel.js delete mode 100644 packages/insomnia-components/components/svg-icon.js delete mode 100644 packages/insomnia-components/components/switch.stories.js delete mode 100644 packages/insomnia-components/components/table.js delete mode 100644 packages/insomnia-components/flow-typed/@storybook/addon-knobs.js delete mode 100644 packages/insomnia-components/flow-typed/@storybook/framer-motion.js delete mode 100644 packages/insomnia-components/flow-typed/autobind-decorator.js delete mode 100644 packages/insomnia-components/flow-typed/class-autobind-decorator.js delete mode 100644 packages/insomnia-components/flow-typed/md5.js delete mode 100644 packages/insomnia-components/flow-typed/npm/classnames_v2.x.x.js delete mode 100644 packages/insomnia-components/flow-typed/npm/styled-components_v4.x.x.js delete mode 100644 packages/insomnia-components/flow-typed/react-dom.js delete mode 100644 packages/insomnia-components/flow-typed/react-switch.js delete mode 100644 packages/insomnia-components/flow-typed/react-use.js delete mode 100644 packages/insomnia-components/index.js create mode 100644 packages/insomnia-components/jest.config.js rename packages/insomnia-components/{ => src}/assets/icn-arrow-right.svg (100%) rename packages/insomnia-components/{ => src}/assets/icn-bitbucket-logo.svg (100%) rename packages/insomnia-components/{ => src}/assets/icn-brackets.svg (100%) rename packages/insomnia-components/{ => src}/assets/icn-burger-menu.svg (100%) rename packages/insomnia-components/{ => src}/assets/icn-checkmark.svg (100%) rename packages/insomnia-components/{ => src}/assets/icn-chevron-down.svg (100%) rename packages/insomnia-components/{ => src}/assets/icn-chevron-up.svg (100%) rename packages/insomnia-components/{ => src}/assets/icn-clock.svg (100%) rename packages/insomnia-components/{ => src}/assets/icn-cookie.svg (100%) rename packages/insomnia-components/{ => src}/assets/icn-drag-grip.svg (100%) rename packages/insomnia-components/{ => src}/assets/icn-elevator.svg (100%) rename packages/insomnia-components/{ => src}/assets/icn-ellipsis-circle.svg (100%) rename packages/insomnia-components/{ => src}/assets/icn-ellipsis.svg (100%) rename packages/insomnia-components/{ => src}/assets/icn-empty.svg (100%) rename packages/insomnia-components/{ => src}/assets/icn-errors.svg (100%) rename packages/insomnia-components/{ => src}/assets/icn-file.svg (100%) rename packages/insomnia-components/{ => src}/assets/icn-folder-open.svg (100%) rename packages/insomnia-components/{ => src}/assets/icn-folder.svg (100%) rename packages/insomnia-components/{ => src}/assets/icn-gear.svg (100%) rename packages/insomnia-components/{ => src}/assets/icn-git-branch.svg (100%) rename packages/insomnia-components/{ => src}/assets/icn-github-logo.svg (100%) rename packages/insomnia-components/{ => src}/assets/icn-gitlab-logo.svg (100%) rename packages/insomnia-components/{ => src}/assets/icn-gui.svg (100%) rename packages/insomnia-components/{ => src}/assets/icn-indentation.svg (100%) rename packages/insomnia-components/{ => src}/assets/icn-info.svg (100%) rename packages/insomnia-components/{ => src}/assets/icn-key.svg (100%) rename packages/insomnia-components/{ => src}/assets/icn-minus-circle-fill.svg (100%) rename packages/insomnia-components/{ => src}/assets/icn-minus-circle.svg (100%) rename packages/insomnia-components/{ => src}/assets/icn-placeholder.svg (100%) rename packages/insomnia-components/{ => src}/assets/icn-play.svg (100%) rename packages/insomnia-components/{ => src}/assets/icn-plus.svg (100%) rename packages/insomnia-components/{ => src}/assets/icn-prohibited.svg (100%) rename packages/insomnia-components/{ => src}/assets/icn-question-fill.svg (100%) rename packages/insomnia-components/{ => src}/assets/icn-question.svg (100%) rename packages/insomnia-components/{ => src}/assets/icn-search.svg (100%) rename packages/insomnia-components/{ => src}/assets/icn-sec-cert.svg (100%) rename packages/insomnia-components/{ => src}/assets/icn-success.svg (100%) rename packages/insomnia-components/{ => src}/assets/icn-sync.svg (100%) rename packages/insomnia-components/{ => src}/assets/icn-trashcan.svg (100%) rename packages/insomnia-components/{ => src}/assets/icn-triangle.svg (100%) rename packages/insomnia-components/{ => src}/assets/icn-user.svg (100%) rename packages/insomnia-components/{ => src}/assets/icn-warning-circle.svg (100%) rename packages/insomnia-components/{ => src}/assets/icn-warning.svg (100%) rename packages/insomnia-components/{ => src}/assets/icn-x.svg (100%) create mode 100644 packages/insomnia-components/src/breadcrumb.stories.tsx rename packages/insomnia-components/{components/breadcrumb.js => src/breadcrumb.tsx} (63%) rename packages/insomnia-components/{components/button/async-button.stories.js => src/button/async-button.stories.tsx} (59%) create mode 100644 packages/insomnia-components/src/button/async-button.tsx rename packages/insomnia-components/{components/button/button.stories.js => src/button/button.stories.tsx} (73%) rename packages/insomnia-components/{components/button/button.js => src/button/button.tsx} (81%) rename packages/insomnia-components/{components/button/circle-button.stories.js => src/button/circle-button.stories.tsx} (82%) rename packages/insomnia-components/{components/button/circle-button.js => src/button/circle-button.tsx} (56%) create mode 100644 packages/insomnia-components/src/button/index.ts create mode 100644 packages/insomnia-components/src/card-container.tsx rename packages/insomnia-components/{components/card.stories.js => src/card.stories.tsx} (97%) rename packages/insomnia-components/{components/card.js => src/card.tsx} (87%) rename packages/insomnia-components/{components/dropdown/dropdown-divider.js => src/dropdown/dropdown-divider.tsx} (80%) rename packages/insomnia-components/{components/dropdown/dropdown-item.js => src/dropdown/dropdown-item.tsx} (72%) rename packages/insomnia-components/{components/dropdown/dropdown.stories.js => src/dropdown/dropdown.stories.tsx} (89%) rename packages/insomnia-components/{components/dropdown/dropdown.js => src/dropdown/dropdown.tsx} (67%) create mode 100644 packages/insomnia-components/src/dropdown/index.ts create mode 100644 packages/insomnia-components/src/header.stories.tsx rename packages/insomnia-components/{components/header.js => src/header.tsx} (77%) rename packages/insomnia-components/{components/help-tooltip.stories.js => src/help-tooltip.stories.tsx} (78%) rename packages/insomnia-components/{components/help-tooltip.js => src/help-tooltip.tsx} (50%) create mode 100644 packages/insomnia-components/src/index.ts rename packages/insomnia-components/{__jest__/setup-after-env.js => src/jest/setup-after-env.ts} (58%) create mode 100644 packages/insomnia-components/src/list-group/index.ts rename packages/insomnia-components/{components/list-group/list-group-item.js => src/list-group/list-group-item.tsx} (61%) rename packages/insomnia-components/{components/list-group/list-group.stories.js => src/list-group/list-group.stories.tsx} (88%) rename packages/insomnia-components/{components/list-group/list-group.js => src/list-group/list-group.tsx} (54%) rename packages/insomnia-components/{components/list-group/unit-test-item.js => src/list-group/unit-test-item.tsx} (56%) rename packages/insomnia-components/{components/list-group/unit-test-request-selector.js => src/list-group/unit-test-request-selector.tsx} (72%) create mode 100644 packages/insomnia-components/src/list-group/unit-test-result-badge.tsx create mode 100644 packages/insomnia-components/src/list-group/unit-test-result-item.tsx rename packages/insomnia-components/{components/list-group/unit-test-result-timestamp.js => src/list-group/unit-test-result-timestamp.tsx} (58%) create mode 100644 packages/insomnia-components/src/multi-switch.stories.tsx rename packages/insomnia-components/{components/multi-switch.js => src/multi-switch.tsx} (60%) rename packages/insomnia-components/{components/notice-table.stories.js => src/notice-table.stories.tsx} (65%) rename packages/insomnia-components/{components/notice-table.js => src/notice-table.tsx} (73%) create mode 100644 packages/insomnia-components/src/radio-button-group.stories.tsx rename packages/insomnia-components/{components/radio-button-group.js => src/radio-button-group.tsx} (65%) create mode 100644 packages/insomnia-components/src/sidebar/index.tsx rename packages/insomnia-components/{components/sidebar/sidebar-badge.js => src/sidebar/sidebar-badge.tsx} (84%) rename packages/insomnia-components/{components/sidebar/sidebar-filter.js => src/sidebar/sidebar-filter.tsx} (65%) rename packages/insomnia-components/{components/sidebar/sidebar-header.js => src/sidebar/sidebar-header.tsx} (67%) rename packages/insomnia-components/{components/sidebar/sidebar-headers.js => src/sidebar/sidebar-headers.tsx} (61%) create mode 100644 packages/insomnia-components/src/sidebar/sidebar-info.tsx create mode 100644 packages/insomnia-components/src/sidebar/sidebar-invalid-section.tsx rename packages/insomnia-components/{components/sidebar/sidebar-item.js => src/sidebar/sidebar-item.tsx} (81%) create mode 100644 packages/insomnia-components/src/sidebar/sidebar-panel.tsx rename packages/insomnia-components/{components/sidebar/sidebar-parameters.js => src/sidebar/sidebar-parameters.tsx} (61%) rename packages/insomnia-components/{components/sidebar/sidebar-paths.js => src/sidebar/sidebar-paths.tsx} (57%) rename packages/insomnia-components/{components/sidebar/sidebar-requests.js => src/sidebar/sidebar-requests.tsx} (77%) rename packages/insomnia-components/{components/sidebar/sidebar-responses.js => src/sidebar/sidebar-responses.tsx} (64%) rename packages/insomnia-components/{components/sidebar/sidebar-schemas.js => src/sidebar/sidebar-schemas.tsx} (62%) rename packages/insomnia-components/{components/sidebar/sidebar-section.js => src/sidebar/sidebar-section.tsx} (60%) rename packages/insomnia-components/{components/sidebar/sidebar-security.js => src/sidebar/sidebar-security.tsx} (59%) rename packages/insomnia-components/{components/sidebar/sidebar-servers.js => src/sidebar/sidebar-servers.tsx} (56%) rename packages/insomnia-components/{components/sidebar/sidebar-text-item.js => src/sidebar/sidebar-text-item.tsx} (50%) rename packages/insomnia-components/{components/sidebar/sidebar.stories.js => src/sidebar/sidebar.stories.tsx} (98%) rename packages/insomnia-components/{components/sidebar/index.js => src/sidebar/sidebar.tsx} (65%) rename packages/insomnia-components/{components/svg-icon.stories.js => src/svg-icon.stories.tsx} (75%) create mode 100644 packages/insomnia-components/src/svg-icon.tsx create mode 100644 packages/insomnia-components/src/switch.stories.tsx rename packages/insomnia-components/{components/switch.js => src/switch.tsx} (74%) rename packages/insomnia-components/{components/table.stories.js => src/table.stories.tsx} (96%) create mode 100644 packages/insomnia-components/src/table.tsx rename packages/insomnia-components/{components/toggle-switch.stories.js => src/toggle-switch.stories.tsx} (67%) rename packages/insomnia-components/{components/toggle-switch.js => src/toggle-switch.tsx} (55%) rename packages/insomnia-components/{components/tooltip.stories.js => src/tooltip.stories.tsx} (88%) rename packages/insomnia-components/{components/__tests__/tooltip.test.js => src/tooltip.test.tsx} (82%) rename packages/insomnia-components/{components/tooltip.js => src/tooltip.tsx} (70%) create mode 100644 packages/insomnia-components/svgr.config.js create mode 100644 packages/insomnia-components/tsconfig.build.json create mode 100644 packages/insomnia-components/tsconfig.json create mode 100644 packages/insomnia-cookies/.eslintignore create mode 100644 packages/insomnia-cookies/jest.config.js rename packages/insomnia-cookies/{__tests__/index.test.js => src/cookies.test.ts} (79%) rename packages/insomnia-cookies/{index.js => src/cookies.ts} (52%) create mode 100644 packages/insomnia-cookies/src/index.ts create mode 100644 packages/insomnia-cookies/src/tough-cookie.d.ts create mode 100644 packages/insomnia-cookies/tsconfig.build.json create mode 100644 packages/insomnia-cookies/tsconfig.json create mode 100644 packages/insomnia-importers/.eslintignore create mode 100644 packages/insomnia-importers/.eslintrc.js delete mode 100755 packages/insomnia-importers/index.js create mode 100644 packages/insomnia-importers/jest.config.js rename packages/insomnia-importers/src/{cli.js => cli.ts} (74%) mode change 100755 => 100644 rename packages/insomnia-importers/src/{__tests__/import-errors.test.js => convert.test.ts} (70%) mode change 100755 => 100644 create mode 100644 packages/insomnia-importers/src/convert.ts create mode 100644 packages/insomnia-importers/src/entities.ts create mode 100644 packages/insomnia-importers/src/importers/apiconnect-wsdl.d.ts delete mode 100755 packages/insomnia-importers/src/importers/curl.js create mode 100644 packages/insomnia-importers/src/importers/curl.ts rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/curl/complex-input.sh (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/curl/complex-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/curl/dollar-sign-input.sh (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/curl/dollar-sign-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/curl/form-input.sh (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/curl/form-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/curl/from-chrome-input.sh (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/curl/from-chrome-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/curl/get-input.sh (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/curl/get-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/curl/header-colon-input.sh (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/curl/header-colon-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/curl/multi-data-input.sh (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/curl/multi-data-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/curl/multi-input.sh (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/curl/multi-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/curl/no-url-input.sh (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/curl/no-url-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/curl/question-mark-input.sh (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/curl/question-mark-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/curl/simple-url-input.sh (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/curl/simple-url-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/curl/skip-squished-input.sh (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/curl/squished-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/curl/url-only-input.sh (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/curl/url-only-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/curl/urlencoded-input.sh (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/curl/urlencoded-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/har/deep-input.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/har/deep-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/har/form-data-input.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/har/form-data-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/har/minimal-input.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/har/minimal-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/har/no-requests-input.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/har/no-requests-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/har/shallow-input.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/har/shallow-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/insomnia-1/complex-input.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/insomnia-1/complex-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/insomnia-1/form-input.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/insomnia-1/form-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/insomnia-1/minimal-input.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/insomnia-1/minimal-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/insomnia-2/complex-input.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/insomnia-2/complex-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/insomnia-3/basic-input.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/insomnia-3/basic-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/insomnia-4/basic-input.yaml (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/insomnia-4/basic-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/openapi3/dereferenced-input.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/openapi3/dereferenced-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/openapi3/dereferenced-with-tags-input.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/openapi3/dereferenced-with-tags-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/openapi3/endpoint-security-input.yaml (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/openapi3/endpoint-security-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/openapi3/example-with-server-variables-input.yaml (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/openapi3/example-with-server-variables-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/openapi3/example-without-servers-input.yaml (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/openapi3/example-without-servers-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/openapi3/global-security-input.yaml (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/openapi3/global-security-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/openapi3/path-plugin-input.yaml (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/openapi3/path-plugin-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/openapi3/petstore-input.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/openapi3/petstore-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/openapi3/petstore-readonly-input.yml (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/openapi3/petstore-readonly-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/openapi3/petstore-with-tags-input.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/openapi3/petstore-with-tags-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/openapi3/petstore-yml-input.yml (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/openapi3/petstore-yml-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/openapi3/petstore-yml-with-tags-input.yml (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/openapi3/petstore-yml-with-tags-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/postman-env/basic-input.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/postman-env/basic-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/postman-env/no-name-input.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/postman-env/no-name-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/postman/aws-signature-auth-v2_0-input.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/postman/aws-signature-auth-v2_0-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/postman/aws-signature-auth-v2_1-input.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/postman/aws-signature-auth-v2_1-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/postman/basic-auth-v2_0-input.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/postman/basic-auth-v2_0-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/postman/basic-auth-v2_1-input.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/postman/basic-auth-v2_1-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/postman/bearer-token-v2_0-input.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/postman/bearer-token-v2_0-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/postman/bearer-token-v2_1-input.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/postman/bearer-token-v2_1-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/postman/complex-url-v2_0-input.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/postman/complex-url-v2_0-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/postman/complex-url-v2_1-input.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/postman/complex-url-v2_1-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/postman/complex-v2_0-input.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/postman/complex-v2_0-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/postman/complex-v2_1-input.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/postman/complex-v2_1-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/postman/digest-auth-v2_0-input.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/postman/digest-auth-v2_0-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/postman/digest-auth-v2_1-input.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/postman/digest-auth-v2_1-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/postman/minimal-v2_0-input.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/postman/minimal-v2_0-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/postman/minimal-v2_1-input.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/postman/minimal-v2_1-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/postman/oauth1_0-auth-v2_0-input.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/postman/oauth1_0-auth-v2_0-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/postman/oauth1_0-auth-v2_1-input.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/postman/oauth1_0-auth-v2_1-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/postman/oauth2_0-auth-v2_0-input.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/postman/oauth2_0-auth-v2_0-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/postman/oauth2_0-auth-v2_1-input.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/postman/oauth2_0-auth-v2_1-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/swagger2/dereferenced-input.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/swagger2/dereferenced-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/swagger2/dereferenced-with-tags-input.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/swagger2/dereferenced-with-tags-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/swagger2/petstore-input.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/swagger2/petstore-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/swagger2/petstore-with-tags-input.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/swagger2/petstore-with-tags-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/swagger2/petstore-yml-input.yml (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/swagger2/petstore-yml-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/swagger2/petstore-yml-with-tags-input.yml (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/swagger2/petstore-yml-with-tags-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/swagger2/user-example-input.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/swagger2/user-example-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/wsdl/addition-input.wsdl (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/wsdl/addition-output.json (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/wsdl/calculator-input.wsdl (100%) rename packages/insomnia-importers/src/{__tests__ => importers}/fixtures/wsdl/calculator-output.json (100%) delete mode 100755 packages/insomnia-importers/src/importers/har.js create mode 100644 packages/insomnia-importers/src/importers/har.ts rename packages/insomnia-importers/src/{__tests__/fixtures.test.js => importers/index.test.ts} (55%) mode change 100755 => 100644 create mode 100644 packages/insomnia-importers/src/importers/index.ts delete mode 100755 packages/insomnia-importers/src/importers/insomnia-1.js create mode 100644 packages/insomnia-importers/src/importers/insomnia-1.ts delete mode 100755 packages/insomnia-importers/src/importers/insomnia-2.js create mode 100644 packages/insomnia-importers/src/importers/insomnia-2.ts delete mode 100755 packages/insomnia-importers/src/importers/insomnia-3.js create mode 100644 packages/insomnia-importers/src/importers/insomnia-3.ts delete mode 100644 packages/insomnia-importers/src/importers/insomnia-4.js create mode 100644 packages/insomnia-importers/src/importers/insomnia-4.ts create mode 100644 packages/insomnia-importers/src/importers/openapi-3.ts delete mode 100644 packages/insomnia-importers/src/importers/openapi3.js create mode 100644 packages/insomnia-importers/src/importers/postman-2.0.types.ts create mode 100644 packages/insomnia-importers/src/importers/postman-2.1.types.ts delete mode 100755 packages/insomnia-importers/src/importers/postman-env.js create mode 100644 packages/insomnia-importers/src/importers/postman-env.ts delete mode 100755 packages/insomnia-importers/src/importers/postman.js create mode 100644 packages/insomnia-importers/src/importers/postman.ts create mode 100644 packages/insomnia-importers/src/importers/swagger-2.ts delete mode 100644 packages/insomnia-importers/src/importers/swagger2.js rename packages/insomnia-importers/src/importers/{wsdl.js => wsdl.ts} (50%) create mode 100755 packages/insomnia-importers/src/index.ts delete mode 100755 packages/insomnia-importers/src/utils.js rename packages/insomnia-importers/src/{__tests__/utils.test.js => utils.test.ts} (62%) mode change 100755 => 100644 create mode 100644 packages/insomnia-importers/src/utils.ts create mode 100644 packages/insomnia-importers/tsconfig.build.json create mode 100644 packages/insomnia-importers/tsconfig.json delete mode 100644 packages/insomnia-inso/.babelrc create mode 100644 packages/insomnia-inso/.eslintignore create mode 100644 packages/insomnia-inso/.eslintrc.js delete mode 100644 packages/insomnia-inso/.flowconfig delete mode 100644 packages/insomnia-inso/__jest__/before.js delete mode 100644 packages/insomnia-inso/__mocks__/cosmiconfig.js delete mode 100644 packages/insomnia-inso/__mocks__/enquirer.js delete mode 100644 packages/insomnia-inso/__mocks__/insomnia-send-request.js delete mode 100644 packages/insomnia-inso/__mocks__/insomnia-testing.js delete mode 100644 packages/insomnia-inso/flow-typed/@stoplight/spectral.js delete mode 100644 packages/insomnia-inso/flow-typed/commander.js delete mode 100644 packages/insomnia-inso/flow-typed/consola.js delete mode 100644 packages/insomnia-inso/flow-typed/cosmiconfig.js delete mode 100644 packages/insomnia-inso/flow-typed/enquirer.js delete mode 100644 packages/insomnia-inso/flow-typed/execa.js delete mode 100644 packages/insomnia-inso/flow-typed/get-bin-path.js delete mode 100644 packages/insomnia-inso/flow-typed/insomnia-send-request.js delete mode 100644 packages/insomnia-inso/flow-typed/insomnia-testing.js delete mode 100644 packages/insomnia-inso/flow-typed/jest.js delete mode 100644 packages/insomnia-inso/flow-typed/lodash.flattendeep.js delete mode 100644 packages/insomnia-inso/flow-typed/mkdirp.js delete mode 100644 packages/insomnia-inso/flow-typed/nedb.js delete mode 100644 packages/insomnia-inso/flow-typed/openapi-2-kong.js delete mode 100644 packages/insomnia-inso/flow-typed/string-argv.js delete mode 100644 packages/insomnia-inso/flow-typed/yaml.js create mode 100644 packages/insomnia-inso/jest.config.js create mode 100644 packages/insomnia-inso/src/__mocks__/cosmiconfig.ts rename packages/insomnia-inso/src/__mocks__/{util.js => util.ts} (99%) rename packages/insomnia-inso/src/{__tests__/__snapshots__/inso-snapshot.test.js.snap => __snapshots__/inso-snapshot.test.ts.snap} (98%) rename packages/insomnia-inso/src/{__tests__/cli.test.js => cli.test.ts} (84%) rename packages/insomnia-inso/src/{cli.js => cli.ts} (54%) mode change 100755 => 100644 create mode 100644 packages/insomnia-inso/src/commands/__mocks__/insomnia-send-request.ts create mode 100644 packages/insomnia-inso/src/commands/__mocks__/insomnia-testing.ts rename packages/insomnia-inso/{__mocks__/openapi-2-kong.js => src/commands/__mocks__/openapi-2-kong.ts} (56%) rename packages/insomnia-inso/src/commands/{__tests__/export-specification.test.js => export-specification.test.ts} (76%) rename packages/insomnia-inso/src/commands/{export-specification.js => export-specification.ts} (76%) rename packages/insomnia-inso/src/commands/{__fixtures__ => fixtures}/openapi-spec.yaml (100%) rename packages/insomnia-inso/src/commands/{__tests__/generate-config.test.js => generate-config.test.ts} (70%) rename packages/insomnia-inso/src/commands/{generate-config.js => generate-config.ts} (57%) rename packages/insomnia-inso/src/commands/{__tests__/lint-specification.test.js => lint-specification.test.ts} (76%) rename packages/insomnia-inso/src/commands/{lint-specification.js => lint-specification.ts} (82%) rename packages/insomnia-inso/src/commands/{__tests__/run-tests.test.js => run-tests.test.ts} (63%) rename packages/insomnia-inso/src/commands/{run-tests.js => run-tests.ts} (58%) rename packages/insomnia-inso/src/{__tests__/data-directory.test.js => data-directory.test.ts} (77%) rename packages/insomnia-inso/src/{data-directory.js => data-directory.ts} (71%) rename packages/insomnia-inso/src/db/__mocks__/{index.js => index.ts} (98%) rename packages/insomnia-inso/src/db/adapters/{__tests__/git-adapter.test.js => git-adapter.test.ts} (92%) rename packages/insomnia-inso/src/db/adapters/{git-adapter.js => git-adapter.ts} (65%) delete mode 100644 packages/insomnia-inso/src/db/adapters/ne-db-adapter.js rename packages/insomnia-inso/src/db/adapters/{__tests__/ne-db-adapter.test.js => ne-db-adapter.test.ts} (92%) create mode 100644 packages/insomnia-inso/src/db/adapters/ne-db-adapter.ts rename packages/insomnia-inso/src/db/{__fixtures__ => fixtures}/git-repo-malformed-spec/.insomnia/ApiSpec/spc_46c5a4a40e83445a9bd9d9758b86c16c.yml (100%) rename packages/insomnia-inso/src/db/{__fixtures__ => fixtures}/git-repo-malformed-spec/.insomnia/Environment/env_ca046a738f001eb3090261a537b1b78f86c2094c.yml (100%) rename packages/insomnia-inso/src/db/{__fixtures__ => fixtures}/git-repo-malformed-spec/.insomnia/Environment/env_env_ca046a738f001eb3090261a537b1b78f86c2094c_sub.yml (100%) rename packages/insomnia-inso/src/db/{__fixtures__ => fixtures}/git-repo-malformed-spec/.insomnia/Request/req_wrk_012d4860c7da418a85ffea7406e1292a21946b60.yml (100%) rename packages/insomnia-inso/src/db/{__fixtures__ => fixtures}/git-repo-malformed-spec/.insomnia/Request/req_wrk_012d4860c7da418a85ffea7406e1292ab410454b.yml (100%) rename packages/insomnia-inso/src/db/{__fixtures__ => fixtures}/git-repo-malformed-spec/.insomnia/RequestGroup/fld_wrk_012d4860c7da418a85ffea7406e1292a30baa249.yml (100%) rename packages/insomnia-inso/src/db/{__fixtures__ => fixtures}/git-repo-malformed-spec/.insomnia/Workspace/wrk_012d4860c7da418a85ffea7406e1292a.yml (100%) rename packages/insomnia-inso/src/db/{__fixtures__ => fixtures}/git-repo/.insomnia/ApiSpec/spc_46c5a4a40e83445a9bd9d9758b86c16c.yml (100%) rename packages/insomnia-inso/src/db/{__fixtures__ => fixtures}/git-repo/.insomnia/Environment/env_ca046a738f001eb3090261a537b1b78f86c2094c.yml (100%) rename packages/insomnia-inso/src/db/{__fixtures__ => fixtures}/git-repo/.insomnia/Environment/env_env_ca046a738f001eb3090261a537b1b78f86c2094c_sub.yml (100%) rename packages/insomnia-inso/src/db/{__fixtures__ => fixtures}/git-repo/.insomnia/Request/req_wrk_012d4860c7da418a85ffea7406e1292a21946b60.yml (100%) rename packages/insomnia-inso/src/db/{__fixtures__ => fixtures}/git-repo/.insomnia/Request/req_wrk_012d4860c7da418a85ffea7406e1292ab410454b.yml (100%) rename packages/insomnia-inso/src/db/{__fixtures__ => fixtures}/git-repo/.insomnia/RequestGroup/fld_wrk_012d4860c7da418a85ffea7406e1292a30baa249.yml (100%) rename packages/insomnia-inso/src/db/{__fixtures__ => fixtures}/git-repo/.insomnia/UnitTest/ut_6b214a8cd07046f2a8f43237ca913c73.yml (100%) rename packages/insomnia-inso/src/db/{__fixtures__ => fixtures}/git-repo/.insomnia/UnitTest/ut_8c0c020fb9e341c2a3162e723445faf1.yml (100%) rename packages/insomnia-inso/src/db/{__fixtures__ => fixtures}/git-repo/.insomnia/UnitTest/ut_8fc42ec7f82a45d09e691ddfe212fd85.yml (100%) rename packages/insomnia-inso/src/db/{__fixtures__ => fixtures}/git-repo/.insomnia/UnitTest/ut_97a197104e83454ebf5f4e0dc0ba4bc6.yml (100%) rename packages/insomnia-inso/src/db/{__fixtures__ => fixtures}/git-repo/.insomnia/UnitTestSuite/uts_7f0f85548b0147f4ba6b5e442d137613.yml (100%) rename packages/insomnia-inso/src/db/{__fixtures__ => fixtures}/git-repo/.insomnia/UnitTestSuite/uts_fe901c6565044f00aa620d3fe47f443f.yml (100%) rename packages/insomnia-inso/src/db/{__fixtures__ => fixtures}/git-repo/.insomnia/Workspace/wrk_012d4860c7da418a85ffea7406e1292a.yml (100%) rename packages/insomnia-inso/src/db/{__fixtures__ => fixtures}/nedb/insomnia.ApiSpec.db (100%) rename packages/insomnia-inso/src/db/{__fixtures__ => fixtures}/nedb/insomnia.Environment.db (100%) rename packages/insomnia-inso/src/db/{__fixtures__ => fixtures}/nedb/insomnia.Request.db (100%) rename packages/insomnia-inso/src/db/{__fixtures__ => fixtures}/nedb/insomnia.RequestGroup.db (100%) rename packages/insomnia-inso/src/db/{__fixtures__ => fixtures}/nedb/insomnia.UnitTest.db (100%) rename packages/insomnia-inso/src/db/{__fixtures__ => fixtures}/nedb/insomnia.UnitTestSuite.db (100%) rename packages/insomnia-inso/src/db/{__fixtures__ => fixtures}/nedb/insomnia.Workspace.db (100%) rename packages/insomnia-inso/src/db/{__tests__/index.test.js => index.test.ts} (74%) rename packages/insomnia-inso/src/db/{index.js => index.ts} (69%) create mode 100644 packages/insomnia-inso/src/db/models/__mocks__/enquirer.ts rename packages/insomnia-inso/src/db/models/{__tests__/__snapshots__/api-spec.test.js.snap => __snapshots__/api-spec.test.ts.snap} (100%) rename packages/insomnia-inso/src/db/models/{__tests__/__snapshots__/environment.test.js.snap => __snapshots__/environment.test.ts.snap} (100%) rename packages/insomnia-inso/src/db/models/{__tests__/__snapshots__/unit-test-suite.test.js.snap => __snapshots__/unit-test-suite.test.ts.snap} (100%) delete mode 100644 packages/insomnia-inso/src/db/models/api-spec.js rename packages/insomnia-inso/src/db/models/{__tests__/api-spec.test.js => api-spec.test.ts} (83%) create mode 100644 packages/insomnia-inso/src/db/models/api-spec.ts rename packages/insomnia-inso/src/db/models/{__tests__/environment.test.js => environment.test.ts} (81%) rename packages/insomnia-inso/src/db/models/{environment.js => environment.ts} (68%) delete mode 100644 packages/insomnia-inso/src/db/models/types.js create mode 100644 packages/insomnia-inso/src/db/models/types.ts rename packages/insomnia-inso/src/db/models/{__tests__/unit-test-suite.test.js => unit-test-suite.test.ts} (84%) rename packages/insomnia-inso/src/db/models/{unit-test-suite.js => unit-test-suite.ts} (55%) delete mode 100644 packages/insomnia-inso/src/db/models/util.js create mode 100644 packages/insomnia-inso/src/db/models/util.ts delete mode 100644 packages/insomnia-inso/src/db/models/workspace.js rename packages/insomnia-inso/src/db/models/{__tests__/workspace.test.js => workspace.test.ts} (72%) create mode 100644 packages/insomnia-inso/src/db/models/workspace.ts rename packages/insomnia-inso/src/{errors.js => errors.ts} (88%) create mode 100644 packages/insomnia-inso/src/fixtures/.insorc-blank.yaml rename packages/insomnia-inso/src/{__fixtures__ => fixtures}/.insorc-missing-properties.yaml (100%) rename packages/insomnia-inso/src/{__fixtures__ => fixtures}/.insorc-with-scripts.yaml (100%) rename packages/insomnia-inso/src/{__fixtures__ => fixtures}/.insorc.yaml (100%) rename packages/insomnia-inso/src/{__tests__/get-options.test.js => get-options.test.ts} (73%) rename packages/insomnia-inso/src/{get-options.js => get-options.ts} (58%) create mode 100644 packages/insomnia-inso/src/global.d.ts create mode 100644 packages/insomnia-inso/src/index.ts rename packages/insomnia-inso/src/{__tests__/inso-snapshot.test.js => inso-snapshot.test.ts} (70%) rename packages/insomnia-inso/{__mocks__/node-libcurl.js => src/jest/__mocks__/node-libcurl.ts} (85%) create mode 100644 packages/insomnia-inso/src/jest/before.ts rename packages/insomnia-inso/{__jest__/setup.js => src/jest/setup.ts} (99%) delete mode 100644 packages/insomnia-inso/src/logger.js rename packages/insomnia-inso/src/{__tests__/logger.test.js => logger.test.ts} (75%) create mode 100644 packages/insomnia-inso/src/logger.ts create mode 100644 packages/insomnia-inso/src/openapi-2-kong.d.ts create mode 100644 packages/insomnia-inso/src/types.ts rename packages/insomnia-inso/src/{__tests__/util.test.js => util.test.ts} (86%) rename packages/insomnia-inso/src/{util.js => util.ts} (51%) rename packages/insomnia-inso/src/{__tests__/write-file.test.js => write-file.test.ts} (82%) rename packages/insomnia-inso/src/{write-file.js => write-file.ts} (98%) create mode 100644 packages/insomnia-inso/tsconfig.build.json create mode 100644 packages/insomnia-inso/tsconfig.json create mode 100644 packages/insomnia-prettify/.eslintignore create mode 100644 packages/insomnia-prettify/.eslintrc.js delete mode 100644 packages/insomnia-prettify/index.js create mode 100644 packages/insomnia-prettify/jest.config.js rename packages/insomnia-prettify/src/{__fixtures__ => fixtures}/escaped-characters-input.json (100%) rename packages/insomnia-prettify/src/{__fixtures__ => fixtures}/escaped-characters-output.json (100%) rename packages/insomnia-prettify/src/{__fixtures__ => fixtures}/escaped-unicode-input.json (100%) rename packages/insomnia-prettify/src/{__fixtures__ => fixtures}/escaped-unicode-output.json (100%) rename packages/insomnia-prettify/src/{__fixtures__ => fixtures}/extra-whitespace-input.json (100%) rename packages/insomnia-prettify/src/{__fixtures__ => fixtures}/extra-whitespace-output.json (100%) rename packages/insomnia-prettify/src/{__fixtures__ => fixtures}/minimal-whitespace-input.json (100%) rename packages/insomnia-prettify/src/{__fixtures__ => fixtures}/minimal-whitespace-output.json (100%) rename packages/insomnia-prettify/src/{__fixtures__ => fixtures}/nunjucks-input.json (100%) rename packages/insomnia-prettify/src/{__fixtures__ => fixtures}/nunjucks-output.json (100%) rename packages/insomnia-prettify/src/{__fixtures__ => fixtures}/precision-input.json (100%) rename packages/insomnia-prettify/src/{__fixtures__ => fixtures}/precision-output.json (100%) rename packages/insomnia-prettify/src/{__fixtures__ => fixtures}/root-array-input.json (100%) rename packages/insomnia-prettify/src/{__fixtures__ => fixtures}/root-array-output.json (100%) rename packages/insomnia-prettify/src/{__fixtures__ => fixtures}/root-string-input.json (100%) rename packages/insomnia-prettify/src/{__fixtures__ => fixtures}/root-string-output.json (100%) rename packages/insomnia-prettify/src/{__fixtures__ => fixtures}/trailing-comma-input.json (100%) rename packages/insomnia-prettify/src/{__fixtures__ => fixtures}/trailing-comma-output.json (100%) rename packages/insomnia-prettify/src/{__fixtures__ => fixtures}/unquoted-strings-input.json (100%) rename packages/insomnia-prettify/src/{__fixtures__ => fixtures}/unquoted-strings-output.json (100%) create mode 100644 packages/insomnia-prettify/src/index.ts rename packages/insomnia-prettify/src/{__tests__/json.test.js => json.test.ts} (78%) rename packages/insomnia-prettify/src/{json.js => json.ts} (78%) create mode 100644 packages/insomnia-prettify/tsconfig.build.json create mode 100644 packages/insomnia-prettify/tsconfig.json delete mode 100644 packages/insomnia-testing/.babelrc create mode 100644 packages/insomnia-testing/.eslintignore create mode 100644 packages/insomnia-testing/.eslintrc.js delete mode 100644 packages/insomnia-testing/.flowconfig delete mode 100644 packages/insomnia-testing/flow-typed/axios.js delete mode 100644 packages/insomnia-testing/flow-typed/chai.js delete mode 100644 packages/insomnia-testing/flow-typed/jest.js delete mode 100644 packages/insomnia-testing/flow-typed/mkdirp.js delete mode 100644 packages/insomnia-testing/flow-typed/mocha.js delete mode 100644 packages/insomnia-testing/index.js create mode 100644 packages/insomnia-testing/jest.config.js delete mode 100644 packages/insomnia-testing/src/__mocks__/axios.js create mode 100644 packages/insomnia-testing/src/__mocks__/axios.ts rename packages/insomnia-testing/src/generate/{__fixtures__ => fixtures}/01_empty.input.json (100%) rename packages/insomnia-testing/src/generate/{__fixtures__ => fixtures}/01_empty.output.js (100%) rename packages/insomnia-testing/src/generate/{__fixtures__ => fixtures}/02_empty-suite.input.json (100%) rename packages/insomnia-testing/src/generate/{__fixtures__ => fixtures}/02_empty-suite.output.js (100%) rename packages/insomnia-testing/src/generate/{__fixtures__ => fixtures}/03_basic-suite.input.json (100%) rename packages/insomnia-testing/src/generate/{__fixtures__ => fixtures}/03_basic-suite.output.js (100%) rename packages/insomnia-testing/src/generate/{__fixtures__ => fixtures}/04_nested-suite.input.json (100%) rename packages/insomnia-testing/src/generate/{__fixtures__ => fixtures}/04_nested-suite.output.js (100%) rename packages/insomnia-testing/src/generate/{__fixtures__ => fixtures}/05_complex-suite.input.json (100%) rename packages/insomnia-testing/src/generate/{__fixtures__ => fixtures}/05_complex-suite.output.js (100%) rename packages/insomnia-testing/src/generate/{__tests__/fixtures.test.js => generate.test.ts} (63%) rename packages/insomnia-testing/src/generate/{index.js => generate.ts} (57%) create mode 100644 packages/insomnia-testing/src/generate/index.ts rename packages/insomnia-testing/src/generate/{__tests__/util.test.js => util.test.ts} (68%) rename packages/insomnia-testing/src/generate/{util.js => util.ts} (50%) create mode 100644 packages/insomnia-testing/src/index.ts rename packages/insomnia-testing/src/{__tests__/integration.test.js => integration/integration.test.ts} (76%) create mode 100644 packages/insomnia-testing/src/run/entities.ts delete mode 100644 packages/insomnia-testing/src/run/index.js create mode 100644 packages/insomnia-testing/src/run/index.ts delete mode 100644 packages/insomnia-testing/src/run/insomnia.js create mode 100644 packages/insomnia-testing/src/run/insomnia.ts delete mode 100644 packages/insomnia-testing/src/run/java-script-reporter.js create mode 100644 packages/insomnia-testing/src/run/javascript-reporter.ts rename packages/insomnia-testing/src/run/{__tests__/index.test.js => run.test.ts} (76%) create mode 100644 packages/insomnia-testing/src/run/run.ts create mode 100644 packages/insomnia-testing/tsconfig.build.json create mode 100644 packages/insomnia-testing/tsconfig.json create mode 100644 packages/insomnia-url/.eslintignore create mode 100644 packages/insomnia-url/.eslintrc.js delete mode 100644 packages/insomnia-url/index.js create mode 100644 packages/insomnia-url/jest.config.js create mode 100644 packages/insomnia-url/src/index.ts rename packages/insomnia-url/src/{__tests__/protocol.test.js => protocol.test.ts} (94%) rename packages/insomnia-url/src/{protocol.js => protocol.ts} (65%) rename packages/insomnia-url/src/{__tests__/querystring.test.js => querystring.test.ts} (97%) rename packages/insomnia-url/src/{querystring.js => querystring.ts} (69%) create mode 100644 packages/insomnia-url/tsconfig.build.json create mode 100644 packages/insomnia-url/tsconfig.json create mode 100644 packages/insomnia-xpath/.eslintignore delete mode 100644 packages/insomnia-xpath/__tests__/index.test.js delete mode 100644 packages/insomnia-xpath/index.js create mode 100644 packages/insomnia-xpath/jest.config.js create mode 100644 packages/insomnia-xpath/src/index.ts create mode 100644 packages/insomnia-xpath/src/query.test.ts create mode 100644 packages/insomnia-xpath/src/query.ts create mode 100644 packages/insomnia-xpath/tsconfig.build.json create mode 100644 packages/insomnia-xpath/tsconfig.json delete mode 100644 packages/openapi-2-kong/.babelrc create mode 100644 packages/openapi-2-kong/.eslintignore create mode 100644 packages/openapi-2-kong/.eslintrc.js delete mode 100644 packages/openapi-2-kong/.flowconfig delete mode 100644 packages/openapi-2-kong/flow-typed/jest.js delete mode 100644 packages/openapi-2-kong/flow-typed/slugify.js delete mode 100644 packages/openapi-2-kong/flow-typed/swagger-parser.js delete mode 100644 packages/openapi-2-kong/flow-typed/url-join.js create mode 100644 packages/openapi-2-kong/jest.config.js rename packages/openapi-2-kong/src/{__tests__/common.test.js => common.test.ts} (68%) rename packages/openapi-2-kong/src/{common.js => common.ts} (80%) rename packages/openapi-2-kong/src/declarative-config/{index.js => generate.ts} (86%) create mode 100644 packages/openapi-2-kong/src/declarative-config/index.ts rename packages/openapi-2-kong/src/declarative-config/{__tests__/names.test.js => names.test.ts} (79%) rename packages/openapi-2-kong/src/declarative-config/{__tests__/plugins.test.js => plugins.test.ts} (87%) rename packages/openapi-2-kong/src/declarative-config/{plugins.js => plugins.ts} (72%) rename packages/openapi-2-kong/src/declarative-config/{__tests__/security-plugins.test.js => security-plugins.test.ts} (60%) rename packages/openapi-2-kong/src/declarative-config/{security-plugins.js => security-plugins.ts} (60%) rename packages/openapi-2-kong/src/declarative-config/{__tests__/services.test.js => services.test.ts} (81%) rename packages/openapi-2-kong/src/declarative-config/{services.js => services.ts} (88%) rename packages/openapi-2-kong/src/declarative-config/{__tests__/upstreams.test.js => upstreams.test.ts} (89%) rename packages/openapi-2-kong/src/declarative-config/{upstreams.js => upstreams.ts} (72%) rename packages/openapi-2-kong/src/declarative-config/{__tests__/utils.js => utils.ts} (68%) rename packages/openapi-2-kong/src/{__tests__/fixtures.test.js => fixtures.test.ts} (80%) rename packages/openapi-2-kong/src/{__fixtures__ => fixtures}/api-with-examples.expected.json (100%) rename packages/openapi-2-kong/src/{__fixtures__ => fixtures}/api-with-examples.yaml (100%) rename packages/openapi-2-kong/src/{__fixtures__ => fixtures}/callback-example.expected.json (100%) rename packages/openapi-2-kong/src/{__fixtures__ => fixtures}/callback-example.yaml (100%) rename packages/openapi-2-kong/src/{__fixtures__ => fixtures}/httpbin.expected.json (100%) rename packages/openapi-2-kong/src/{__fixtures__ => fixtures}/httpbin.yaml (100%) rename packages/openapi-2-kong/src/{__fixtures__ => fixtures}/link-example.expected.json (100%) rename packages/openapi-2-kong/src/{__fixtures__ => fixtures}/link-example.yaml (100%) rename packages/openapi-2-kong/src/{__fixtures__ => fixtures}/no-targets-example.expected.json (100%) rename packages/openapi-2-kong/src/{__fixtures__ => fixtures}/no-targets-example.yaml (100%) rename packages/openapi-2-kong/src/{__fixtures__ => fixtures}/petstore-expanded.expected.json (100%) rename packages/openapi-2-kong/src/{__fixtures__ => fixtures}/petstore-expanded.yaml (100%) rename packages/openapi-2-kong/src/{__fixtures__ => fixtures}/petstore.expected.json (100%) rename packages/openapi-2-kong/src/{__fixtures__ => fixtures}/petstore.yaml (100%) rename packages/openapi-2-kong/src/{__fixtures__ => fixtures}/request-validator-plugin.expected.json (100%) rename packages/openapi-2-kong/src/{__fixtures__ => fixtures}/request-validator-plugin.yaml (100%) rename packages/openapi-2-kong/src/{__fixtures__ => fixtures}/security.expected.json (100%) rename packages/openapi-2-kong/src/{__fixtures__ => fixtures}/security.yaml (100%) rename packages/openapi-2-kong/src/{__fixtures__ => fixtures}/uspto.expected.json (100%) rename packages/openapi-2-kong/src/{__fixtures__ => fixtures}/uspto.yaml (100%) rename packages/openapi-2-kong/src/{__tests__/index.test.js => generate.test.ts} (80%) rename packages/openapi-2-kong/src/{index.js => generate.ts} (68%) create mode 100644 packages/openapi-2-kong/src/index.ts rename packages/openapi-2-kong/src/kubernetes/{__tests__/index.test.js => generate.test.ts} (84%) rename packages/openapi-2-kong/src/kubernetes/{index.js => generate.ts} (79%) create mode 100644 packages/openapi-2-kong/src/kubernetes/index.ts rename packages/openapi-2-kong/src/kubernetes/{__tests__/util/plugin-helpers.js => plugin-helpers.ts} (71%) rename packages/openapi-2-kong/src/kubernetes/{__tests__/plugins.test.js => plugins.test.ts} (88%) rename packages/openapi-2-kong/src/kubernetes/{plugins.js => plugins.ts} (72%) rename packages/openapi-2-kong/src/kubernetes/{__tests__/variables.test.js => variables.test.ts} (95%) rename packages/openapi-2-kong/src/kubernetes/{variables.js => variables.ts} (95%) create mode 100644 packages/openapi-2-kong/src/types/declarative-config.ts create mode 100644 packages/openapi-2-kong/src/types/k8plugins.ts create mode 100644 packages/openapi-2-kong/src/types/kubernetes-config.ts create mode 100644 packages/openapi-2-kong/src/types/openapi3.ts create mode 100644 packages/openapi-2-kong/src/types/outputs.ts create mode 100644 packages/openapi-2-kong/tsconfig.build.json create mode 100644 packages/openapi-2-kong/tsconfig.json delete mode 100644 packages/openapi-2-kong/types/declarative-config.flow.js delete mode 100644 packages/openapi-2-kong/types/k8plugins.flow.js delete mode 100644 packages/openapi-2-kong/types/kubernetes-config.flow.js delete mode 100644 packages/openapi-2-kong/types/openapi3.flow.js delete mode 100644 packages/openapi-2-kong/types/outputs.flow.js delete mode 100644 packages/openapi-2-kong/webpack.config.js create mode 100755 scripts/convert-to-typescript create mode 100644 tsconfig.base.json create mode 100644 tsconfig.eslint.json diff --git a/.editorconfig b/.editorconfig index 5cf5ac2d95..b0ad35f528 100644 --- a/.editorconfig +++ b/.editorconfig @@ -5,3 +5,4 @@ indent_size = 2 insert_final_newline = true trim_trailing_whitespace = true end_of_line = lf +quote_type = single diff --git a/.eslintignore b/.eslintignore index 293ffdaa77..221cd0faf1 100644 --- a/.eslintignore +++ b/.eslintignore @@ -8,13 +8,12 @@ screenshots/ **/.cache/ **/coverage/ **/node_modules/ -**/webpack/ **/bin/ **/__fixtures__/ +**/fixtures **/__snapshots__/ -**/flow-typed/ **/dist/ **/.cache/ **/svgr/ **/storybook-static/ -*.md +*.md \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000000..6fcc223c66 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,101 @@ +/** @type { import('eslint').Linter.Config } */ +module.exports = { + parser: '@typescript-eslint/parser', + parserOptions: { + project: [ + './tsconfig.eslint.json', + './packages/*/tsconfig.json', + './plugins/*/tsconfig.json', + ], + tsconfigRootDir: __dirname, + ecmaFeatures: { + jsx: true, + }, + }, + extends: [ + 'plugin:@typescript-eslint/eslint-recommended', + 'plugin:@typescript-eslint/recommended', + 'semistandard', + 'plugin:react-hooks/recommended', + ], + plugins: [ + '@typescript-eslint', + 'react', + 'jest', + 'html', + 'json', + 'filenames', + 'react-hooks', + ], + globals: { + __DEV__: true, + fail: true, + NodeJS: true, + HTMLDivElement: true, + HTMLElement: true, + HTMLInputElement: true, + HTMLSelectElement: true, + JSX: true, + }, + env: { + browser: true, + commonjs: true, + es6: true, + 'jest/globals': true, + node: true, + }, + overrides: [ + { + files: ['*.js'], + rules: { + '@typescript-eslint/no-var-requires': 'off', + }, + }, + ], + rules: { + 'comma-dangle': ['error', 'always-multiline'], + indent: 'off', + 'no-var': 'error', + 'no-async-promise-executor': 'off', + 'no-case-declarations': 'off', + 'no-prototype-builtins': 'off', + 'no-duplicate-imports': 'off', + 'react/jsx-uses-react': 'error', + 'react/jsx-uses-vars': 'error', + 'space-in-parens': 'off', + 'react-hooks/rules-of-hooks': 'error', + 'react-hooks/exhaustive-deps': 'error', + camelcase: ['error', { allow: ['__export_format', '__export_date', '__export_source'] }], + 'space-before-function-paren': [ + 'error', + { + anonymous: 'never', + named: 'never', + asyncArrow: 'always', + }, + ], + 'filenames/match-exported': [ + 'error', + 'kebab', + ], + '@typescript-eslint/explicit-module-boundary-types': 'off', + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': ['error', { ignoreRestSiblings: true }], + '@typescript-eslint/ban-types': 'off', + '@typescript-eslint/no-empty-function': 'off', + '@typescript-eslint/no-namespace': ['error', { allowDeclarations: true }], + 'spaced-comment': ['error', 'always', { + exceptions: ['/', '*', '-', '* '], // for ASCII art :) + markers: [ + '/', // for TypeScript directives, doxygen, vsdoc, etc. (which use `///`) + '?', // for Quokka + ], + }], + '@typescript-eslint/array-type': ['error', { default: 'array', readonly: 'array' }], + '@typescript-eslint/consistent-type-definitions': ['error', 'interface'], + quotes: 'off', + '@typescript-eslint/quotes': ['error', 'single', { avoidEscape: true }], + 'no-use-before-define': 'off', + '@typescript-eslint/no-use-before-define': 'error', + }, +}; diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 0674b6bc80..0000000000 --- a/.eslintrc.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "parser": "babel-eslint", - "extends": ["semistandard", "plugin:flowtype/recommended", "plugin:prettier/recommended", "plugin:react-hooks/recommended"], - "plugins": ["react", "jest", "html", "json", "filenames", "flowtype", "react-hooks"], - "parserOptions": { - "ecmaFeatures": { - "jsx": true - } - }, - "globals": { - "__DEV__": true, - "fail": true, - "HTMLDivElement": true, - "HTMLElement": true, - "HTMLInputElement": true, - "HTMLSelectElement": true - }, - "env": { - "jest/globals": true - }, - "rules": { - "comma-dangle": ["error", "always-multiline"], - "indent": "off", - "no-var": "error", - "no-async-promise-executor": "off", - "no-case-declarations": "off", - "no-prototype-builtins": "off", - "no-duplicate-imports": "off", - "flowtype/space-after-type-colon": "off", - "react/jsx-uses-react": "error", - "react/jsx-uses-vars": "error", - "space-in-parens": "off", - "react-hooks/rules-of-hooks": "error", - "react-hooks/exhaustive-deps": "error", - "space-before-function-paren": [ - "error", - { - "anonymous": "never", - "named": "never", - "asyncArrow": "always" - } - ], - "filenames/match-exported": ["error", "kebab"], - "flowtype/array-style-simple-type": "error", - "flowtype/array-style-complex-type": "error" - }, - "settings": { - "flowtype": { - "onlyFilesWithFlowAnnotation": true - } - } -} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 16f28c8525..ccb62da08b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,6 +28,8 @@ jobs: node-version: ${{ steps.nvm.outputs.NVMRC }} - name: Bootstrap packages run: npm run bootstrap + - name: Lint + run: npm run lint - name: Run tests run: npm test - name: Build for smoke tests diff --git a/.gitignore b/.gitignore index 012d4bb754..aca846eeba 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,5 @@ graphql.config.json .graphqlconfig schema.graphql packages/insomnia-smoke-test/screenshots +*.tsbuildinfo +dist \ No newline at end of file diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 7e27ffbdd7..0000000000 --- a/.prettierignore +++ /dev/null @@ -1,7 +0,0 @@ -**/bin/* -**/build/* -**/dist/* -**/__fixtures__/* -**/flow-typed/* -*.md -**/__snapshots__/ diff --git a/.prettierrc b/.prettierrc deleted file mode 100644 index 1001dcf271..0000000000 --- a/.prettierrc +++ /dev/null @@ -1,9 +0,0 @@ -{ - "bracketSpacing": true, - "jsxBracketSameLine": true, - "semi": true, - "singleQuote": true, - "tabWidth": 2, - "trailingComma": "all", - "printWidth": 100 -} diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000000..31aa926f2b --- /dev/null +++ b/jest.config.js @@ -0,0 +1,20 @@ +/** @type { import('@jest/types').Config.InitialOptions } */ +module.exports = { + globals: { + 'ts-jest': { + isolatedModules: true, + }, + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], + testEnvironment: 'node', + transform: { + '^.+\\.tsx?$': 'ts-jest', + }, + testMatch: [ + '**/*.test.ts', + ], + verbose: false, + resetMocks: true, + resetModules: true, + collectCoverage: false, +}; diff --git a/package-lock.json b/package-lock.json index db438723a3..f9bd28dfea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41,6 +41,12 @@ "readdirp": "^2.2.1", "upath": "^1.1.1" } + }, + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true } } }, @@ -65,43 +71,276 @@ } }, "@babel/core": { - "version": "7.10.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.10.5.tgz", - "integrity": "sha512-O34LQooYVDXPl7QWCdW9p4NR+QlzOr7xShPPJz8GsuCU3/8ua/wqTr7gmnxXv+WBESiGU/G5s16i6tUvHkNb+w==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.14.0.tgz", + "integrity": "sha512-8YqpRig5NmIHlMLw09zMlPTvUVMILjqCOtVgu+TVNWEBvy9b5I3RRyhqnrV4hjgEK7n8P9OqvkWJAFmEL6Wwfw==", "dev": true, "requires": { - "@babel/code-frame": "^7.10.4", - "@babel/generator": "^7.10.5", - "@babel/helper-module-transforms": "^7.10.5", - "@babel/helpers": "^7.10.4", - "@babel/parser": "^7.10.5", - "@babel/template": "^7.10.4", - "@babel/traverse": "^7.10.5", - "@babel/types": "^7.10.5", + "@babel/code-frame": "^7.12.13", + "@babel/generator": "^7.14.0", + "@babel/helper-compilation-targets": "^7.13.16", + "@babel/helper-module-transforms": "^7.14.0", + "@babel/helpers": "^7.14.0", + "@babel/parser": "^7.14.0", + "@babel/template": "^7.12.13", + "@babel/traverse": "^7.14.0", + "@babel/types": "^7.14.0", "convert-source-map": "^1.7.0", "debug": "^4.1.0", - "gensync": "^1.0.0-beta.1", + "gensync": "^1.0.0-beta.2", "json5": "^2.1.2", - "lodash": "^4.17.19", - "resolve": "^1.3.2", - "semver": "^5.4.1", + "semver": "^6.3.0", "source-map": "^0.5.0" }, "dependencies": { - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "@babel/code-frame": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", + "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", "dev": true, "requires": { - "ms": "^2.1.1" + "@babel/highlight": "^7.12.13" } }, + "@babel/compat-data": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.14.0.tgz", + "integrity": "sha512-vu9V3uMM/1o5Hl5OekMUowo3FqXLJSw+s+66nt0fSWVWTtmosdzn45JHOB3cPtZoe6CTBDzvSw0RdOY85Q37+Q==", + "dev": true + }, + "@babel/generator": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.1.tgz", + "integrity": "sha512-TMGhsXMXCP/O1WtQmZjpEYDhCYC9vFhayWZPJSZCGkPJgUqX0rF0wwtrYvnzVxIjcF80tkUertXVk5cwqi5cAQ==", + "dev": true, + "requires": { + "@babel/types": "^7.14.1", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.13.16", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.13.16.tgz", + "integrity": "sha512-3gmkYIrpqsLlieFwjkGgLaSHmhnvlAYzZLlYVjlW+QwI+1zE17kGxuJGmIqDQdYp56XdmGeD+Bswx0UTyG18xA==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.13.15", + "@babel/helper-validator-option": "^7.12.17", + "browserslist": "^4.14.5", + "semver": "^6.3.0" + } + }, + "@babel/helper-function-name": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz", + "integrity": "sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.12.13", + "@babel/template": "^7.12.13", + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz", + "integrity": "sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg==", + "dev": true, + "requires": { + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.13.12", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.13.12.tgz", + "integrity": "sha512-48ql1CLL59aKbU94Y88Xgb2VFy7a95ykGRbJJaaVv+LX5U8wFpLfiGXJJGUozsmA1oEh/o5Bp60Voq7ACyA/Sw==", + "dev": true, + "requires": { + "@babel/types": "^7.13.12" + } + }, + "@babel/helper-module-imports": { + "version": "7.13.12", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.13.12.tgz", + "integrity": "sha512-4cVvR2/1B693IuOvSI20xqqa/+bl7lqAMR59R4iu39R9aOX8/JoYY1sFaNvUMyMBGnHdwvJgUrzNLoUZxXypxA==", + "dev": true, + "requires": { + "@babel/types": "^7.13.12" + } + }, + "@babel/helper-module-transforms": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.14.0.tgz", + "integrity": "sha512-L40t9bxIuGOfpIGA3HNkJhU9qYrf4y5A5LUSw7rGMSn+pcG8dfJ0g6Zval6YJGd2nEjI7oP00fRdnhLKndx6bw==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.13.12", + "@babel/helper-replace-supers": "^7.13.12", + "@babel/helper-simple-access": "^7.13.12", + "@babel/helper-split-export-declaration": "^7.12.13", + "@babel/helper-validator-identifier": "^7.14.0", + "@babel/template": "^7.12.13", + "@babel/traverse": "^7.14.0", + "@babel/types": "^7.14.0" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.12.13.tgz", + "integrity": "sha512-BdWQhoVJkp6nVjB7nkFWcn43dkprYauqtk++Py2eaf/GRDFm5BxRqEIZCiHlZUGAVmtwKcsVL1dC68WmzeFmiA==", + "dev": true, + "requires": { + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-replace-supers": { + "version": "7.13.12", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.13.12.tgz", + "integrity": "sha512-Gz1eiX+4yDO8mT+heB94aLVNCL+rbuT2xy4YfyNqu8F+OI6vMvJK891qGBTqL9Uc8wxEvRW92Id6G7sDen3fFw==", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.13.12", + "@babel/helper-optimise-call-expression": "^7.12.13", + "@babel/traverse": "^7.13.0", + "@babel/types": "^7.13.12" + } + }, + "@babel/helper-simple-access": { + "version": "7.13.12", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.13.12.tgz", + "integrity": "sha512-7FEjbrx5SL9cWvXioDbnlYTppcZGuCY6ow3/D5vMggb2Ywgu4dMrpTJX0JdQAIcRRUElOIxF3yEooa9gUb9ZbA==", + "dev": true, + "requires": { + "@babel/types": "^7.13.12" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz", + "integrity": "sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg==", + "dev": true, + "requires": { + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz", + "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==", + "dev": true + }, + "@babel/highlight": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz", + "integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.14.0", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.1.tgz", + "integrity": "sha512-muUGEKu8E/ftMTPlNp+mc6zL3E9zKWmF5sDHZ5MSsoTP9Wyz64AhEf9kD08xYJ7w6Hdcu8H550ircnPyWSIF0Q==", + "dev": true + }, + "@babel/template": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz", + "integrity": "sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@babel/parser": "^7.12.13", + "@babel/types": "^7.12.13" + } + }, + "@babel/traverse": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.0.tgz", + "integrity": "sha512-dZ/a371EE5XNhTHomvtuLTUyx6UEoJmYX+DT5zBCQN3McHemsuIaKKYqsc/fs26BEkHs/lBZy0J571LP5z9kQA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@babel/generator": "^7.14.0", + "@babel/helper-function-name": "^7.12.13", + "@babel/helper-split-export-declaration": "^7.12.13", + "@babel/parser": "^7.14.0", + "@babel/types": "^7.14.0", + "debug": "^4.1.0", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.1.tgz", + "integrity": "sha512-S13Qe85fzLs3gYRUnrpyeIrBJIMYv33qSTg1qoBwiG6nPKwUWAD9odSzWhEedpwOIzSEI6gbdQIWEMiCI42iBA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.14.0", + "to-fast-properties": "^2.0.0" + } + }, + "browserslist": { + "version": "4.16.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz", + "integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001219", + "colorette": "^1.2.2", + "electron-to-chromium": "^1.3.723", + "escalade": "^3.1.1", + "node-releases": "^1.1.71" + } + }, + "caniuse-lite": { + "version": "1.0.30001227", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001227.tgz", + "integrity": "sha512-HHZT4QpUw4Pf45IE3xxKAPgAN3q2aRai/x5TSHP8lrrKzARoH0IeBviwStcRi5lsFlscTkBbXC7gXyms43151A==", + "dev": true + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "electron-to-chromium": { + "version": "1.3.727", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.727.tgz", + "integrity": "sha512-Mfz4FIB4FSvEwBpDfdipRIrwd6uo8gUDoRDF4QEYb4h4tSuI3ov594OrjU6on042UlFHouIJpClDODGkPcBSbg==", + "dev": true + }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true + }, + "node-releases": { + "version": "1.1.71", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.71.tgz", + "integrity": "sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg==", + "dev": true + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true } } }, @@ -351,6 +590,12 @@ "integrity": "sha512-3U9y+43hz7ZM+rzG24Qe2mufW5KhvFg/NhnNph+i9mgCtdTCtMJuI1TMkrIUiK7Ix4PYlRF9I5dhqaLYA/ADXw==", "dev": true }, + "@babel/helper-validator-option": { + "version": "7.12.17", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.12.17.tgz", + "integrity": "sha512-TopkMDmLzq8ngChwRlyjR6raKD6gMSae4JdYDB8bByKreQgG0RBTuKe9LRxW3wFtUnjxOPRKBDwEH6Mg5KeDfw==", + "dev": true + }, "@babel/helper-wrap-function": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.10.4.tgz", @@ -364,14 +609,140 @@ } }, "@babel/helpers": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.10.4.tgz", - "integrity": "sha512-L2gX/XeUONeEbI78dXSrJzGdz4GQ+ZTA/aazfUsFaWjSe95kiCuOZ5HsXvkiw3iwF+mFHSRUfJU8t6YavocdXA==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.14.0.tgz", + "integrity": "sha512-+ufuXprtQ1D1iZTO/K9+EBRn+qPWMJjZSw/S0KlFrxCw4tkrzv9grgpDHkY9MeQTjTY8i2sp7Jep8DfU6tN9Mg==", "dev": true, "requires": { - "@babel/template": "^7.10.4", - "@babel/traverse": "^7.10.4", - "@babel/types": "^7.10.4" + "@babel/template": "^7.12.13", + "@babel/traverse": "^7.14.0", + "@babel/types": "^7.14.0" + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.13.tgz", + "integrity": "sha512-HV1Cm0Q3ZrpCR93tkWOYiuYIgLxZXZFVG2VgK+MBWjUqZTundupbfx2aXarXuw5Ko5aMcjtJgbSs4vUGBS5v6g==", + "dev": true, + "requires": { + "@babel/highlight": "^7.12.13" + } + }, + "@babel/generator": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.14.1.tgz", + "integrity": "sha512-TMGhsXMXCP/O1WtQmZjpEYDhCYC9vFhayWZPJSZCGkPJgUqX0rF0wwtrYvnzVxIjcF80tkUertXVk5cwqi5cAQ==", + "dev": true, + "requires": { + "@babel/types": "^7.14.1", + "jsesc": "^2.5.1", + "source-map": "^0.5.0" + } + }, + "@babel/helper-function-name": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz", + "integrity": "sha512-TZvmPn0UOqmvi5G4vvw0qZTpVptGkB1GL61R6lKvrSdIxGm5Pky7Q3fpKiIkQCAtRCBUwB0PaThlx9vebCDSwA==", + "dev": true, + "requires": { + "@babel/helper-get-function-arity": "^7.12.13", + "@babel/template": "^7.12.13", + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-get-function-arity": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz", + "integrity": "sha512-DjEVzQNz5LICkzN0REdpD5prGoidvbdYk1BVgRUOINaWJP2t6avB27X1guXK1kXNrX0WMfsrm1A/ZBthYuIMQg==", + "dev": true, + "requires": { + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz", + "integrity": "sha512-tCJDltF83htUtXx5NLcaDqRmknv652ZWCHyoTETf1CXYJdPC7nohZohjUgieXhv0hTJdRf2FjDueFehdNucpzg==", + "dev": true, + "requires": { + "@babel/types": "^7.12.13" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.0.tgz", + "integrity": "sha512-V3ts7zMSu5lfiwWDVWzRDGIN+lnCEUdaXgtVHJgLb1rGaA6jMrtB9EmE7L18foXJIE8Un/A/h6NJfGQp/e1J4A==", + "dev": true + }, + "@babel/highlight": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.0.tgz", + "integrity": "sha512-YSCOwxvTYEIMSGaBQb5kDDsCopDdiUGsqpatp3fOlI4+2HQSkTmEVWnVuySdAC5EWCqSWWTv0ib63RjR7dTBdg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.14.0", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@babel/parser": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.14.1.tgz", + "integrity": "sha512-muUGEKu8E/ftMTPlNp+mc6zL3E9zKWmF5sDHZ5MSsoTP9Wyz64AhEf9kD08xYJ7w6Hdcu8H550ircnPyWSIF0Q==", + "dev": true + }, + "@babel/template": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.12.13.tgz", + "integrity": "sha512-/7xxiGA57xMo/P2GVvdEumr8ONhFOhfgq2ihK3h1e6THqzTAkHbkXgB0xI9yeTfIUoH3+oAeHhqm/I43OTbbjA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@babel/parser": "^7.12.13", + "@babel/types": "^7.12.13" + } + }, + "@babel/traverse": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.14.0.tgz", + "integrity": "sha512-dZ/a371EE5XNhTHomvtuLTUyx6UEoJmYX+DT5zBCQN3McHemsuIaKKYqsc/fs26BEkHs/lBZy0J571LP5z9kQA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@babel/generator": "^7.14.0", + "@babel/helper-function-name": "^7.12.13", + "@babel/helper-split-export-declaration": "^7.12.13", + "@babel/parser": "^7.14.0", + "@babel/types": "^7.14.0", + "debug": "^4.1.0", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.14.1.tgz", + "integrity": "sha512-S13Qe85fzLs3gYRUnrpyeIrBJIMYv33qSTg1qoBwiG6nPKwUWAD9odSzWhEedpwOIzSEI6gbdQIWEMiCI42iBA==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.14.0", + "to-fast-properties": "^2.0.0" + } + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } } }, "@babel/highlight": { @@ -1333,6 +1704,15 @@ "which": "^1.3.1" }, "dependencies": { + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1370,125 +1750,25 @@ "dev": true }, "@jest/console": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-25.5.0.tgz", - "integrity": "sha512-T48kZa6MK1Y6k4b89sexwmSF4YLeZS/Udqg3Jj3jG/cHH+N/sLFCEoXEDMOKugJQ9FxPN1osxIknvKkxt6MKyw==", + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-26.6.2.tgz", + "integrity": "sha512-IY1R2i2aLsLr7Id3S6p2BA82GNWryt4oSvEXLAKc+L2zdi89dSkE8xC1C+0kpATG4JhBJREnQOH7/zmccM2B0g==", "dev": true, "requires": { - "@jest/types": "^25.5.0", - "chalk": "^3.0.0", - "jest-message-util": "^25.5.0", - "jest-util": "^25.5.0", + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^26.6.2", + "jest-util": "^26.6.2", "slash": "^3.0.0" }, "dependencies": { "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "@jest/core": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-25.5.4.tgz", - "integrity": "sha512-3uSo7laYxF00Dg/DMgbn4xMJKmDdWvZnf89n8Xj/5/AeQ2dOQmn6b6Hkj/MleyzZWXpwv+WSdYWl4cLsy2JsoA==", - "dev": true, - "requires": { - "@jest/console": "^25.5.0", - "@jest/reporters": "^25.5.1", - "@jest/test-result": "^25.5.0", - "@jest/transform": "^25.5.1", - "@jest/types": "^25.5.0", - "ansi-escapes": "^4.2.1", - "chalk": "^3.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.4", - "jest-changed-files": "^25.5.0", - "jest-config": "^25.5.4", - "jest-haste-map": "^25.5.1", - "jest-message-util": "^25.5.0", - "jest-regex-util": "^25.2.6", - "jest-resolve": "^25.5.1", - "jest-resolve-dependencies": "^25.5.4", - "jest-runner": "^25.5.4", - "jest-runtime": "^25.5.4", - "jest-snapshot": "^25.5.1", - "jest-util": "^25.5.0", - "jest-validate": "^25.5.0", - "jest-watcher": "^25.5.0", - "micromatch": "^4.0.2", - "p-each-series": "^2.1.0", - "realpath-native": "^2.0.0", - "rimraf": "^3.0.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, @@ -1502,9 +1782,9 @@ } }, "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -1547,6 +1827,265 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, + "jest-util": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", + "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + } + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "@jest/core": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-26.6.3.tgz", + "integrity": "sha512-xvV1kKbhfUqFVuZ8Cyo+JPpipAHHAV3kcDBftiduK8EICXmTFddryy3P7NfZt8Pv37rA9nEJBKCCkglCPt/Xjw==", + "dev": true, + "requires": { + "@jest/console": "^26.6.2", + "@jest/reporters": "^26.6.2", + "@jest/test-result": "^26.6.2", + "@jest/transform": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.4", + "jest-changed-files": "^26.6.2", + "jest-config": "^26.6.3", + "jest-haste-map": "^26.6.2", + "jest-message-util": "^26.6.2", + "jest-regex-util": "^26.0.0", + "jest-resolve": "^26.6.2", + "jest-resolve-dependencies": "^26.6.3", + "jest-runner": "^26.6.3", + "jest-runtime": "^26.6.3", + "jest-snapshot": "^26.6.2", + "jest-util": "^26.6.2", + "jest-validate": "^26.6.2", + "jest-watcher": "^26.6.2", + "micromatch": "^4.0.2", + "p-each-series": "^2.1.0", + "rimraf": "^3.0.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "dependencies": { + "@jest/transform": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-26.6.2.tgz", + "integrity": "sha512-E9JjhUgNzvuQ+vVAL21vlyfy12gP0GhazGgJC4h6qUt1jSdUXGWJ1wfu/X7Sd8etSgxV4ovT1pb9v5D6QW4XgA==", + "dev": true, + "requires": { + "@babel/core": "^7.1.0", + "@jest/types": "^26.6.2", + "babel-plugin-istanbul": "^6.0.0", + "chalk": "^4.0.0", + "convert-source-map": "^1.4.0", + "fast-json-stable-stringify": "^2.0.0", + "graceful-fs": "^4.2.4", + "jest-haste-map": "^26.6.2", + "jest-regex-util": "^26.0.0", + "jest-util": "^26.6.2", + "micromatch": "^4.0.2", + "pirates": "^4.0.1", + "slash": "^3.0.0", + "source-map": "^0.6.1", + "write-file-atomic": "^3.0.0" + } + }, + "ansi-regex": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "jest-haste-map": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-26.6.2.tgz", + "integrity": "sha512-easWIJXIw71B2RdR8kgqpjQrbMRWQBgiBwXYEhtGUTaX+doCjBheluShdDMeR8IMfJiTqH4+zfhtg29apJf/8w==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "@types/graceful-fs": "^4.1.2", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "fsevents": "^2.1.2", + "graceful-fs": "^4.2.4", + "jest-regex-util": "^26.0.0", + "jest-serializer": "^26.6.2", + "jest-util": "^26.6.2", + "jest-worker": "^26.6.2", + "micromatch": "^4.0.2", + "sane": "^4.0.3", + "walker": "^1.0.7" + } + }, + "jest-regex-util": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-26.0.0.tgz", + "integrity": "sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A==", + "dev": true + }, + "jest-serializer": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-26.6.2.tgz", + "integrity": "sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g==", + "dev": true, + "requires": { + "@types/node": "*", + "graceful-fs": "^4.2.4" + } + }, + "jest-util": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", + "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + } + }, + "jest-worker": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", + "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", + "dev": true, + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + } + }, "micromatch": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", @@ -1572,6 +2111,12 @@ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, "strip-ansi": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", @@ -1582,9 +2127,9 @@ } }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -1602,212 +2147,37 @@ } }, "@jest/environment": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-25.5.0.tgz", - "integrity": "sha512-U2VXPEqL07E/V7pSZMSQCvV5Ea4lqOlT+0ZFijl/i316cRMHvZ4qC+jBdryd+lmRetjQo0YIQr6cVPNxxK87mA==", + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-26.6.2.tgz", + "integrity": "sha512-nFy+fHl28zUrRsCeMB61VDThV1pVTtlEokBRgqPrcT1JNq4yRNIyTHfyht6PqtUvY9IsuLGTrbG8kPXjSZIZwA==", "dev": true, "requires": { - "@jest/fake-timers": "^25.5.0", - "@jest/types": "^25.5.0", - "jest-mock": "^25.5.0" + "@jest/fake-timers": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "jest-mock": "^26.6.2" } }, "@jest/fake-timers": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-25.5.0.tgz", - "integrity": "sha512-9y2+uGnESw/oyOI3eww9yaxdZyHq7XvprfP/eeoCsjqKYts2yRlsHS/SgjPDV8FyMfn2nbMy8YzUk6nyvdLOpQ==", + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-26.6.2.tgz", + "integrity": "sha512-14Uleatt7jdzefLPYM3KLcnUl1ZNikaKq34enpb5XG9i81JpppDb5muZvonvKyrl7ftEHkKS5L5/eB/kxJ+bvA==", "dev": true, "requires": { - "@jest/types": "^25.5.0", - "jest-message-util": "^25.5.0", - "jest-mock": "^25.5.0", - "jest-util": "^25.5.0", - "lolex": "^5.0.0" - } - }, - "@jest/globals": { - "version": "25.5.2", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-25.5.2.tgz", - "integrity": "sha512-AgAS/Ny7Q2RCIj5kZ+0MuKM1wbF0WMLxbCVl/GOMoCNbODRdJ541IxJ98xnZdVSZXivKpJlNPIWa3QmY0l4CXA==", - "dev": true, - "requires": { - "@jest/environment": "^25.5.0", - "@jest/types": "^25.5.0", - "expect": "^25.5.0" - } - }, - "@jest/reporters": { - "version": "25.5.1", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-25.5.1.tgz", - "integrity": "sha512-3jbd8pPDTuhYJ7vqiHXbSwTJQNavczPs+f1kRprRDxETeE3u6srJ+f0NPuwvOmk+lmunZzPkYWIFZDLHQPkviw==", - "dev": true, - "requires": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^25.5.0", - "@jest/test-result": "^25.5.0", - "@jest/transform": "^25.5.1", - "@jest/types": "^25.5.0", - "chalk": "^3.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.2", - "graceful-fs": "^4.2.4", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^4.0.0", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.0.2", - "jest-haste-map": "^25.5.1", - "jest-resolve": "^25.5.1", - "jest-util": "^25.5.0", - "jest-worker": "^25.5.0", - "node-notifier": "^6.0.0", - "slash": "^3.0.0", - "source-map": "^0.6.0", - "string-length": "^3.1.0", - "terminal-link": "^2.0.0", - "v8-to-istanbul": "^4.1.3" + "@jest/types": "^26.6.2", + "@sinonjs/fake-timers": "^6.0.1", + "@types/node": "*", + "jest-message-util": "^26.6.2", + "jest-mock": "^26.6.2", + "jest-util": "^26.6.2" }, "dependencies": { "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "@jest/source-map": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-25.5.0.tgz", - "integrity": "sha512-eIGx0xN12yVpMcPaVpjXPnn3N30QGJCJQSkEDUt9x1fI1Gdvb07Ml6K5iN2hG7NmMP6FDmtPEssE3z6doOYUwQ==", - "dev": true, - "requires": { - "callsites": "^3.0.0", - "graceful-fs": "^4.2.4", - "source-map": "^0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } - } - }, - "@jest/test-result": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-25.5.0.tgz", - "integrity": "sha512-oV+hPJgXN7IQf/fHWkcS99y0smKLU2czLBJ9WA0jHITLst58HpQMtzSYxzaBvYc6U5U6jfoMthqsUlUlbRXs0A==", - "dev": true, - "requires": { - "@jest/console": "^25.5.0", - "@jest/types": "^25.5.0", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" - } - }, - "@jest/test-sequencer": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-25.5.4.tgz", - "integrity": "sha512-pTJGEkSeg1EkCO2YWq6hbFvKNXk8ejqlxiOg1jBNLnWrgXOkdY6UmqZpwGFXNnRt9B8nO1uWMzLLZ4eCmhkPNA==", - "dev": true, - "requires": { - "@jest/test-result": "^25.5.0", - "graceful-fs": "^4.2.4", - "jest-haste-map": "^25.5.1", - "jest-runner": "^25.5.4", - "jest-runtime": "^25.5.4" - } - }, - "@jest/transform": { - "version": "25.5.1", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-25.5.1.tgz", - "integrity": "sha512-Y8CEoVwXb4QwA6Y/9uDkn0Xfz0finGkieuV0xkdF9UtZGJeLukD5nLkaVrVsODB1ojRWlaoD0AJZpVHCSnJEvg==", - "dev": true, - "requires": { - "@babel/core": "^7.1.0", - "@jest/types": "^25.5.0", - "babel-plugin-istanbul": "^6.0.0", - "chalk": "^3.0.0", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.0.0", - "graceful-fs": "^4.2.4", - "jest-haste-map": "^25.5.1", - "jest-regex-util": "^25.2.6", - "jest-util": "^25.5.0", - "micromatch": "^4.0.2", - "pirates": "^4.0.1", - "realpath-native": "^2.0.0", - "slash": "^3.0.0", - "source-map": "^0.6.1", - "write-file-atomic": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, @@ -1821,9 +2191,9 @@ } }, "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -1866,6 +2236,261 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, + "jest-util": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", + "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + } + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "@jest/globals": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-26.6.2.tgz", + "integrity": "sha512-85Ltnm7HlB/KesBUuALwQ68YTU72w9H2xW9FjZ1eL1U3lhtefjjl5c2MiUbpXt/i6LaPRvoOFJ22yCBSfQ0JIA==", + "dev": true, + "requires": { + "@jest/environment": "^26.6.2", + "@jest/types": "^26.6.2", + "expect": "^26.6.2" + } + }, + "@jest/reporters": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-26.6.2.tgz", + "integrity": "sha512-h2bW53APG4HvkOnVMo8q3QXa6pcaNt1HkwVsOPMBV6LD/q9oSpxNSYZQYkAnjdMjrJ86UuYeLo+aEZClV6opnw==", + "dev": true, + "requires": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^26.6.2", + "@jest/test-result": "^26.6.2", + "@jest/transform": "^26.6.2", + "@jest/types": "^26.6.2", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.2", + "graceful-fs": "^4.2.4", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^4.0.3", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "jest-haste-map": "^26.6.2", + "jest-resolve": "^26.6.2", + "jest-util": "^26.6.2", + "jest-worker": "^26.6.2", + "node-notifier": "^8.0.0", + "slash": "^3.0.0", + "source-map": "^0.6.0", + "string-length": "^4.0.1", + "terminal-link": "^2.0.0", + "v8-to-istanbul": "^7.0.0" + }, + "dependencies": { + "@jest/transform": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-26.6.2.tgz", + "integrity": "sha512-E9JjhUgNzvuQ+vVAL21vlyfy12gP0GhazGgJC4h6qUt1jSdUXGWJ1wfu/X7Sd8etSgxV4ovT1pb9v5D6QW4XgA==", + "dev": true, + "requires": { + "@babel/core": "^7.1.0", + "@jest/types": "^26.6.2", + "babel-plugin-istanbul": "^6.0.0", + "chalk": "^4.0.0", + "convert-source-map": "^1.4.0", + "fast-json-stable-stringify": "^2.0.0", + "graceful-fs": "^4.2.4", + "jest-haste-map": "^26.6.2", + "jest-regex-util": "^26.0.0", + "jest-util": "^26.6.2", + "micromatch": "^4.0.2", + "pirates": "^4.0.1", + "slash": "^3.0.0", + "source-map": "^0.6.1", + "write-file-atomic": "^3.0.0" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "jest-haste-map": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-26.6.2.tgz", + "integrity": "sha512-easWIJXIw71B2RdR8kgqpjQrbMRWQBgiBwXYEhtGUTaX+doCjBheluShdDMeR8IMfJiTqH4+zfhtg29apJf/8w==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "@types/graceful-fs": "^4.1.2", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "fsevents": "^2.1.2", + "graceful-fs": "^4.2.4", + "jest-regex-util": "^26.0.0", + "jest-serializer": "^26.6.2", + "jest-util": "^26.6.2", + "jest-worker": "^26.6.2", + "micromatch": "^4.0.2", + "sane": "^4.0.3", + "walker": "^1.0.7" + } + }, + "jest-regex-util": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-26.0.0.tgz", + "integrity": "sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A==", + "dev": true + }, + "jest-serializer": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-26.6.2.tgz", + "integrity": "sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g==", + "dev": true, + "requires": { + "@types/node": "*", + "graceful-fs": "^4.2.4" + } + }, + "jest-util": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", + "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + } + }, + "jest-worker": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", + "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", + "dev": true, + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + } + }, "micromatch": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", @@ -1889,9 +2514,227 @@ "dev": true }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "@jest/source-map": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-26.6.2.tgz", + "integrity": "sha512-YwYcCwAnNmOVsZ8mr3GfnzdXDAl4LaenZP5z+G0c8bzC9/dugL8zRmxZzdoTl4IaS3CryS1uWnROLPFmb6lVvA==", + "dev": true, + "requires": { + "callsites": "^3.0.0", + "graceful-fs": "^4.2.4", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + } + } + }, + "@jest/test-result": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-26.6.2.tgz", + "integrity": "sha512-5O7H5c/7YlojphYNrK02LlDIV2GNPYisKwHm2QTKjNZeEzezCbwYs9swJySv2UfPMyZ0VdsmMv7jIlD/IKYQpQ==", + "dev": true, + "requires": { + "@jest/console": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + } + }, + "@jest/test-sequencer": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-26.6.3.tgz", + "integrity": "sha512-YHlVIjP5nfEyjlrSr8t/YdNfU/1XEt7c5b4OxcXCjyRhjzLYu/rO69/WHPuYcbCWkz8kAeZVZp2N2+IOLLEPGw==", + "dev": true, + "requires": { + "@jest/test-result": "^26.6.2", + "graceful-fs": "^4.2.4", + "jest-haste-map": "^26.6.2", + "jest-runner": "^26.6.3", + "jest-runtime": "^26.6.3" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "jest-haste-map": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-26.6.2.tgz", + "integrity": "sha512-easWIJXIw71B2RdR8kgqpjQrbMRWQBgiBwXYEhtGUTaX+doCjBheluShdDMeR8IMfJiTqH4+zfhtg29apJf/8w==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "@types/graceful-fs": "^4.1.2", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "fsevents": "^2.1.2", + "graceful-fs": "^4.2.4", + "jest-regex-util": "^26.0.0", + "jest-serializer": "^26.6.2", + "jest-util": "^26.6.2", + "jest-worker": "^26.6.2", + "micromatch": "^4.0.2", + "sane": "^4.0.3", + "walker": "^1.0.7" + } + }, + "jest-regex-util": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-26.0.0.tgz", + "integrity": "sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A==", + "dev": true + }, + "jest-serializer": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-26.6.2.tgz", + "integrity": "sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g==", + "dev": true, + "requires": { + "@types/node": "*", + "graceful-fs": "^4.2.4" + } + }, + "jest-util": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", + "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + } + }, + "jest-worker": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", + "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", + "dev": true, + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + } + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -1909,31 +2752,31 @@ } }, "@jest/types": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-25.5.0.tgz", - "integrity": "sha512-OXD0RgQ86Tu3MazKo8bnrkDRaDXXMGUqd+kTtLtK1Zb7CRzQcaSRPPPV37SvYTdevXEBVxe0HXylEjs8ibkmCw==", + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", + "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", "dev": true, "requires": { "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^1.1.1", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", "@types/yargs": "^15.0.0", - "chalk": "^3.0.0" + "chalk": "^4.0.0" }, "dependencies": { "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -1962,9 +2805,9 @@ "dev": true }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -2224,6 +3067,14 @@ "minimatch": "^3.0.4", "npmlog": "^4.1.2", "slash": "^2.0.0" + }, + "dependencies": { + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + } } }, "@lerna/command": { @@ -2302,6 +3153,12 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "dev": true + }, + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true } } }, @@ -2496,6 +3353,14 @@ "@lerna/symlink-dependencies": "3.17.0", "p-map": "^2.1.0", "slash": "^2.0.0" + }, + "dependencies": { + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + } } }, "@lerna/list": { @@ -2925,6 +3790,15 @@ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", "dev": true + }, + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } } } }, @@ -3073,6 +3947,12 @@ "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "dev": true }, + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + }, "type-fest": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz", @@ -3114,12 +3994,40 @@ "glob-to-regexp": "^0.3.0" } }, + "@nodelib/fs.scandir": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.4.tgz", + "integrity": "sha512-33g3pMJk3bg5nXbL/+CY6I2eJDzZAni49PfJnL5fghPTggPvBd/pFNSgJsdAgWptuFu7qq/ERvOYFlhvsLTCKA==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.4", + "run-parallel": "^1.1.9" + }, + "dependencies": { + "@nodelib/fs.stat": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz", + "integrity": "sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q==", + "dev": true + } + } + }, "@nodelib/fs.stat": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==", "dev": true }, + "@nodelib/fs.walk": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.6.tgz", + "integrity": "sha512-8Broas6vTtW4GIXTAHDoE32hnN2M5ykgCpWGbuXHQ15vEMqr23pB76e/GZcYsZCHALv50ktd24qhEyKr6wBtow==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.4", + "fastq": "^1.6.0" + } + }, "@octokit/auth-token": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.4.2.tgz", @@ -3304,14 +4212,23 @@ } }, "@sinonjs/commons": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.1.tgz", - "integrity": "sha512-892K+kWUUi3cl+LlqEWIDrhvLgdL79tECi8JZUyq6IviKy/DNhuzCRlbHUjxK89f4ypPMMaFnFuR9Ie6DoIMsw==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.2.tgz", + "integrity": "sha512-sruwd86RJHdsVf/AtBoijDmUqJp3B6hF/DGC23C+JaegnDHaZyewCjoVGTdg3J0uz3Zs7NnIT05OBOmML72lQw==", "dev": true, "requires": { "type-detect": "4.0.8" } }, + "@sinonjs/fake-timers": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz", + "integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, "@types/babel__core": { "version": "7.1.9", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.9.tgz", @@ -3353,12 +4270,34 @@ "@babel/types": "^7.3.0" } }, + "@types/chai": { + "version": "4.2.15", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.2.15.tgz", + "integrity": "sha512-rYff6FI+ZTKAPkJUoyz7Udq3GaoDZnxYDEvdEdFZASiA7PoErltHezDishqQiSDWrGxvxmplH304jyzQmjp0AQ==", + "dev": true + }, "@types/color-name": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", "dev": true }, + "@types/eslint": { + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-4.16.1.tgz", + "integrity": "sha512-lRUXQAULl5geixTiP2K0iYvMUbCkEnuOwvLGjwff12I4ECxoW5QaWML5UUOZ1CvpQLILkddBdMPMZz4ByQizsg==", + "dev": true, + "requires": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "@types/estree": { + "version": "0.0.46", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.46.tgz", + "integrity": "sha512-laIjwTQaD+5DukBZaygQ79K1Z0jb1bPEMRrkXSLjtCcZm+abyp5YbrqpSLzD42FwWW6gK/aS4NYpJ804nG2brg==", + "dev": true + }, "@types/glob": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", @@ -3394,15 +4333,24 @@ } }, "@types/istanbul-reports": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz", - "integrity": "sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.0.tgz", + "integrity": "sha512-nwKNbvnwJ2/mndE9ItP/zc2TCzw6uuodnF4EHYWD+gCQDVBuRQL5UzbZD0/ezy1iKsFU2ZQiDqg4M9dN4+wZgA==", "dev": true, "requires": { - "@types/istanbul-lib-coverage": "*", "@types/istanbul-lib-report": "*" } }, + "@types/jest": { + "version": "26.0.23", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-26.0.23.tgz", + "integrity": "sha512-ZHLmWMJ9jJ9PTiT58juykZpL7KjwJywFN3Rr2pTSkyQfydf/rk22yS7W8p5DaVUMQ2BQC7oYiU3FjbTM/mYrOA==", + "dev": true, + "requires": { + "jest-diff": "^26.0.0", + "pretty-format": "^26.0.0" + } + }, "@types/json-schema": { "version": "7.0.5", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.5.tgz", @@ -3428,9 +4376,9 @@ "dev": true }, "@types/node": { - "version": "14.0.27", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.0.27.tgz", - "integrity": "sha512-kVrqXhbclHNHGu9ztnAwSncIgJv/FaxmzXJvGXNdcCpV1b8u1/Mi6z6m0vwy0LzKeXFTPLH0NzwmoJ3fNCIq0g==", + "version": "14.14.35", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.35.tgz", + "integrity": "sha512-Lt+wj8NVPx0zUmUwumiVXapmaLUcAk3yPuHCFVXras9k5VT9TdhJqKqGVUQCD60OTMCl0qxJ57OiTL0Mic3Iag==", "dev": true }, "@types/normalize-package-data": { @@ -3446,32 +4394,351 @@ "dev": true }, "@types/prettier": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-1.19.1.tgz", - "integrity": "sha512-5qOlnZscTn4xxM5MeGXAMOsIOIKIbh9e85zJWfBRVPlRMEVawzoPhINYbRGkBZCI8LxvBe7tJCdWiarA99OZfQ==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.2.3.tgz", + "integrity": "sha512-PijRCG/K3s3w1We6ynUKdxEc5AcuuH3NBmMDP8uvKVp6X43UY7NQlTzczakXP3DJR0F4dfNQIGjU2cUeRYs2AA==", "dev": true }, + "@types/rimraf": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-3.0.0.tgz", + "integrity": "sha512-7WhJ0MdpFgYQPXlF4Dx+DhgvlPCfz/x5mHaeDQAKhcenvQP1KCpLQ18JklAqeGMYSAT2PxLpzd0g2/HE7fj7hQ==", + "dev": true, + "requires": { + "@types/glob": "*", + "@types/node": "*" + } + }, "@types/stack-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", - "integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz", + "integrity": "sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==", "dev": true }, "@types/yargs": { - "version": "15.0.5", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.5.tgz", - "integrity": "sha512-Dk/IDOPtOgubt/IaevIUbTgV7doaKkoorvOyYM2CMwuDyP89bekI7H4xLIwunNYiK9jhCkmc6pUrJk3cj2AB9w==", + "version": "15.0.13", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.13.tgz", + "integrity": "sha512-kQ5JNTrbDv3Rp5X2n/iUu37IJBDU2gsZ5R/g1/KHOOEc5IKfUFjXT6DENPGduh08I/pamwtEq4oul7gUqKTQDQ==", "dev": true, "requires": { "@types/yargs-parser": "*" } }, "@types/yargs-parser": { - "version": "15.0.0", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-15.0.0.tgz", - "integrity": "sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw==", + "version": "20.2.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-20.2.0.tgz", + "integrity": "sha512-37RSHht+gzzgYeobbG+KWryeAW8J33Nhr69cjTqSYymXVZEN9NbRYWoYlRtDhHKPVT1FyNKwaTPC1NynKZpzRA==", "dev": true }, + "@typescript-eslint/eslint-plugin": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.18.0.tgz", + "integrity": "sha512-Lzkc/2+7EoH7+NjIWLS2lVuKKqbEmJhtXe3rmfA8cyiKnZm3IfLf51irnBcmow8Q/AptVV0XBZmBJKuUJTe6cQ==", + "dev": true, + "requires": { + "@typescript-eslint/experimental-utils": "4.18.0", + "@typescript-eslint/scope-manager": "4.18.0", + "debug": "^4.1.1", + "functional-red-black-tree": "^1.0.1", + "lodash": "^4.17.15", + "regexpp": "^3.0.0", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "semver": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "@typescript-eslint/experimental-utils": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.18.0.tgz", + "integrity": "sha512-92h723Kblt9JcT2RRY3QS2xefFKar4ZQFVs3GityOKWQYgtajxt/tuXIzL7sVCUlM1hgreiV5gkGYyBpdOwO6A==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.3", + "@typescript-eslint/scope-manager": "4.18.0", + "@typescript-eslint/types": "4.18.0", + "@typescript-eslint/typescript-estree": "4.18.0", + "eslint-scope": "^5.0.0", + "eslint-utils": "^2.0.0" + } + }, + "@typescript-eslint/parser": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.18.0.tgz", + "integrity": "sha512-W3z5S0ZbecwX3PhJEAnq4mnjK5JJXvXUDBYIYGoweCyWyuvAKfGHvzmpUzgB5L4cRBb+cTu9U/ro66dx7dIimA==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "4.18.0", + "@typescript-eslint/types": "4.18.0", + "@typescript-eslint/typescript-estree": "4.18.0", + "debug": "^4.1.1" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + } + } + }, + "@typescript-eslint/scope-manager": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.18.0.tgz", + "integrity": "sha512-olX4yN6rvHR2eyFOcb6E4vmhDPsfdMyfQ3qR+oQNkAv8emKKlfxTWUXU5Mqxs2Fwe3Pf1BoPvrwZtwngxDzYzQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.18.0", + "@typescript-eslint/visitor-keys": "4.18.0" + } + }, + "@typescript-eslint/types": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.18.0.tgz", + "integrity": "sha512-/BRociARpj5E+9yQ7cwCF/SNOWwXJ3qhjurMuK2hIFUbr9vTuDeu476Zpu+ptxY2kSxUHDGLLKy+qGq2sOg37A==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.18.0.tgz", + "integrity": "sha512-wt4xvF6vvJI7epz+rEqxmoNQ4ZADArGQO9gDU+cM0U5fdVv7N+IAuVoVAoZSOZxzGHBfvE3XQMLdy+scsqFfeg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.18.0", + "@typescript-eslint/visitor-keys": "4.18.0", + "debug": "^4.1.1", + "globby": "^11.0.1", + "is-glob": "^4.0.1", + "semver": "^7.3.2", + "tsutils": "^3.17.1" + }, + "dependencies": { + "@nodelib/fs.stat": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.4.tgz", + "integrity": "sha512-IYlHJA0clt2+Vg7bccq+TzRdJvv19c2INqBSsoOLp1je7xjtr7J26+WXR72MCdvU9q1qTzIWDfhMf+DRvQJK4Q==", + "dev": true + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "fast-glob": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.5.tgz", + "integrity": "sha512-2DtFcgT68wiTTiwZ2hNdJfcHNke9XOfnwmBRWXhmeKM8rF0TGwmC/Qto3S7RoZKp5cilZbxzO5iTNTQsJ+EeDg==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.0", + "merge2": "^1.3.0", + "micromatch": "^4.0.2", + "picomatch": "^2.2.1" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + }, + "globby": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.2.tgz", + "integrity": "sha512-2ZThXDvvV8fYFRVIxnrMQBipZQDr7MxKAmQK1vujaj9/7eF0efG7BPUKJ7jP7G5SLF37xKDXvO4S/KKLj/Z0og==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.1.1", + "ignore": "^5.1.4", + "merge2": "^1.3.0", + "slash": "^3.0.0" + } + }, + "ignore": { + "version": "5.1.8", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", + "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "semver": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "@typescript-eslint/visitor-keys": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.18.0.tgz", + "integrity": "sha512-Q9t90JCvfYaN0OfFUgaLqByOfz8yPeTAdotn/XYNm5q9eHax90gzdb+RJ6E9T5s97Kv/UHWKERTmqA0jTKAEHw==", + "dev": true, + "requires": { + "@typescript-eslint/types": "4.18.0", + "eslint-visitor-keys": "^2.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.0.0.tgz", + "integrity": "sha512-QudtT6av5WXels9WjIM7qz1XD1cWGvX4gGXvp/zBn9nXG02D0utdU3Em2m/QjTnrsk6bBjmCygl3rmj118msQQ==", + "dev": true + } + } + }, "@zkochan/cmd-shim": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@zkochan/cmd-shim/-/cmd-shim-3.1.0.tgz", @@ -3494,9 +4761,9 @@ } }, "abab": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.3.tgz", - "integrity": "sha512-tsFzPpcttalNjFBCFMqsKYQcWxxen1pgJR56by//QwvJc4/OUS3kPOOttx2tSIfjsylB0pYu7f5D3K1RCxUnUg==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", + "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==", "dev": true }, "abbrev": { @@ -3512,21 +4779,13 @@ "dev": true }, "acorn-globals": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-4.3.4.tgz", - "integrity": "sha512-clfQEh21R+D0leSbUdWf3OcfqyaCSAQ8Ryq00bofSekfr9W8u1jyYZo6ir0xu9Gtcf7BjcHJpnbZH7JOCpP60A==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", + "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", "dev": true, "requires": { - "acorn": "^6.0.1", - "acorn-walk": "^6.0.1" - }, - "dependencies": { - "acorn": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", - "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", - "dev": true - } + "acorn": "^7.1.1", + "acorn-walk": "^7.1.1" } }, "acorn-jsx": { @@ -3536,9 +4795,9 @@ "dev": true }, "acorn-walk": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-6.2.0.tgz", - "integrity": "sha512-7evsyfH1cLOCdAzZAd43Cic04yKydNx0cF+7tiA19p1XnLLPU4dpCQOqpjqwokFe//vS0QqfqqjCS2JkiIs0cA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", "dev": true }, "agent-base": { @@ -3701,12 +4960,6 @@ "integrity": "sha512-KbUpJgx909ZscOc/7CLATBFam7P1Z1QRQInvgT0UztM9Q72aGKCunKASAl7WNW0tnPmPyEMeMhdsfWhfmW037w==", "dev": true }, - "array-equal": { - "version": "1.0.0", - "resolved": "http://registry.npmjs.org/array-equal/-/array-equal-1.0.0.tgz", - "integrity": "sha1-jCpe8kcv2ep0KwTHenUJO6J1fJM=", - "dev": true - }, "array-find-index": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", @@ -3848,94 +5101,6 @@ "integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA==", "dev": true }, - "babel-eslint": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", - "integrity": "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.7.0", - "@babel/traverse": "^7.7.0", - "@babel/types": "^7.7.0", - "eslint-visitor-keys": "^1.0.0", - "resolve": "^1.12.0" - } - }, - "babel-jest": { - "version": "25.5.1", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-25.5.1.tgz", - "integrity": "sha512-9dA9+GmMjIzgPnYtkhBg73gOo/RHqPmLruP3BaGL4KEX3Dwz6pI8auSN8G8+iuEG90+GSswyKvslN+JYSaacaQ==", - "dev": true, - "requires": { - "@jest/transform": "^25.5.1", - "@jest/types": "^25.5.0", - "@types/babel__core": "^7.1.7", - "babel-plugin-istanbul": "^6.0.0", - "babel-preset-jest": "^25.5.0", - "chalk": "^3.0.0", - "graceful-fs": "^4.2.4", - "slash": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, "babel-loader": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.1.0.tgz", @@ -3984,21 +5149,10 @@ "test-exclude": "^6.0.0" } }, - "babel-plugin-jest-hoist": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-25.5.0.tgz", - "integrity": "sha512-u+/W+WAjMlvoocYGTwthAiQSxDcJAyHpQ6oWlHdFZaaN+Rlk8Q7iiwDPg2lN/FyJtAYnKjFxbn7xus4HCFkg5g==", - "dev": true, - "requires": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__traverse": "^7.0.6" - } - }, "babel-plugin-styled-components": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-1.11.1.tgz", - "integrity": "sha512-YwrInHyKUk1PU3avIRdiLyCpM++18Rs1NgyMXEAQC33rIXs/vro0A+stf4sT0Gf22Got+xRWB8Cm0tw+qkRzBA==", + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-1.12.0.tgz", + "integrity": "sha512-FEiD7l5ZABdJPpLssKXjBUJMYqzbcNzBowfXDCdJhOpbhWiewapUaY+LZGT8R4Jg2TwOjGjG4RKeyrO5p9sBkA==", "dev": true, "requires": { "@babel/helper-annotate-as-pure": "^7.0.0", @@ -4013,35 +5167,6 @@ "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=", "dev": true }, - "babel-preset-current-node-syntax": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-0.1.3.tgz", - "integrity": "sha512-uyexu1sVwcdFnyq9o8UQYsXwXflIh8LvrF5+cKrYam93ned1CStffB3+BEcsxGSgagoA3GEyjDqO4a/58hyPYQ==", - "dev": true, - "requires": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.8.3", - "@babel/plugin-syntax-import-meta": "^7.8.3", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.8.3", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" - } - }, - "babel-preset-jest": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-25.5.0.tgz", - "integrity": "sha512-8ZczygctQkBU+63DtSOKGh7tFL0CeCuz+1ieud9lJ1WPQ9O6A1a/r+LGn6Y705PA6whHQ3T1XuB/PmpfNYf8Fw==", - "dev": true, - "requires": { - "babel-plugin-jest-hoist": "^25.5.0", - "babel-preset-current-node-syntax": "^0.1.2" - } - }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -4192,23 +5317,6 @@ "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", "dev": true }, - "browser-resolve": { - "version": "1.11.3", - "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz", - "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==", - "dev": true, - "requires": { - "resolve": "1.1.7" - }, - "dependencies": { - "resolve": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", - "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=", - "dev": true - } - } - }, "browserslist": { "version": "4.13.0", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.13.0.tgz", @@ -4221,6 +5329,15 @@ "node-releases": "^1.1.58" } }, + "bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "requires": { + "fast-json-stable-stringify": "2.x" + } + }, "bser": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", @@ -4281,6 +5398,17 @@ "ssri": "^6.0.1", "unique-filename": "^1.1.1", "y18n": "^4.0.0" + }, + "dependencies": { + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } } }, "cache-base": { @@ -4387,6 +5515,12 @@ "supports-color": "^5.3.0" } }, + "char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true + }, "chardet": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", @@ -4405,6 +5539,12 @@ "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", "dev": true }, + "cjs-module-lexer": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-0.6.0.tgz", + "integrity": "sha512-uc2Vix1frTfnuzxxu1Hp4ktSvM3QaI4oXl4ZUqL1wjTu/BGki9TrCWoqLTg/drR1KwAEarXuRFCG2Svr1GxPFw==", + "dev": true + }, "clap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/clap/-/clap-1.2.3.tgz", @@ -4607,9 +5747,9 @@ "dev": true }, "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", "dev": true, "requires": { "emoji-regex": "^8.0.0", @@ -4697,6 +5837,12 @@ "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", "dev": true }, + "colorette": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", + "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==", + "dev": true + }, "colors": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", @@ -5289,6 +6435,17 @@ "mkdirp": "^0.5.1", "rimraf": "^2.5.4", "run-queue": "^1.0.0" + }, + "dependencies": { + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } } }, "copy-descriptor": { @@ -5488,14 +6645,42 @@ } }, "data-urls": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-1.1.0.tgz", - "integrity": "sha512-YTWYI9se1P55u58gL5GkQHW4P6VJBJ5iBT+B5a7i2Tjadhv52paJG0qHX4A0OR6/t52odI64KP2YvFpkDOi3eQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", + "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", "dev": true, "requires": { - "abab": "^2.0.0", - "whatwg-mimetype": "^2.2.0", - "whatwg-url": "^7.0.0" + "abab": "^2.0.3", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.0.0" + }, + "dependencies": { + "tr46": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.0.2.tgz", + "integrity": "sha512-3n1qG+/5kg+jrbTzwAykB5yRYtQCTqOGKq5U5PE3b0a1/mzo6snDhjGS0zJVJunO0NrT3Dg1MLy5TjWP/UJppg==", + "dev": true, + "requires": { + "punycode": "^2.1.1" + } + }, + "webidl-conversions": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", + "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", + "dev": true + }, + "whatwg-url": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.4.0.tgz", + "integrity": "sha512-vwTUFf6V4zhcPkWp/4CQPr1TW9Ml6SF4lVyaIMBdJw5i6qUUJ1QWM4Z6YYVkfka0OUIzVo/0aNtGVGk256IKWw==", + "dev": true, + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^2.0.2", + "webidl-conversions": "^6.1.0" + } + } } }, "dateformat": { @@ -5543,6 +6728,12 @@ } } }, + "decimal.js": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.2.1.tgz", + "integrity": "sha512-KaL7+6Fw6i5A2XSnsbhm/6B+NuEA7TZ4vqxnd5tXz9sbKtrN9Srj8ab4vKVdK8YAqZO9P1kg45Y6YLoduPf+kw==", + "dev": true + }, "decode-uri-component": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", @@ -5667,9 +6858,9 @@ } }, "diff-sequences": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-25.2.6.tgz", - "integrity": "sha512-Hq8o7+6GaZeoFjtpgvRBUknSXNeJiCx7V9Fr94ZMljNiCr9n9L8H8aJqgWOQiDDGdyn29fRNcDdRVJ5fdyihfg==", + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", + "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==", "dev": true }, "dir-glob": { @@ -5708,12 +6899,20 @@ } }, "domexception": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz", - "integrity": "sha512-raigMkn7CJNNo6Ihro1fzG7wr3fHuYVytzquZKX5n0yizGsTcYgzdIUwj1X9pK0VvjeihV+XiclP+DjwbsSKug==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", + "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", "dev": true, "requires": { - "webidl-conversions": "^4.0.2" + "webidl-conversions": "^5.0.0" + }, + "dependencies": { + "webidl-conversions": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", + "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", + "dev": true + } } }, "dot-prop": { @@ -5759,6 +6958,12 @@ "integrity": "sha512-8vb8zKIeGlZigeDzNWWthmGeLzo5CC43Lc+CZshMs7UXFVMPNLtXJGa/txedpu3OJFrXXVheBwp9PqOJJlHQ8w==", "dev": true }, + "emittery": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.7.2.tgz", + "integrity": "sha512-A8OG5SR/ij3SsJdWDJdkkSYUjQdCUx6APQXem0SaEePBSRg4eymGYwBkKo1Y6DU+af/Jn2dBQqDBvjnr9Vi8nQ==", + "dev": true + }, "emoji-regex": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", @@ -5894,18 +7099,24 @@ "dev": true }, "escodegen": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", - "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", + "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==", "dev": true, "requires": { "esprima": "^4.0.1", - "estraverse": "^4.2.0", + "estraverse": "^5.2.0", "esutils": "^2.0.2", "optionator": "^0.8.1", "source-map": "~0.6.1" }, "dependencies": { + "estraverse": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", + "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", + "dev": true + }, "levn": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", @@ -6075,6 +7286,14 @@ "dev": true, "requires": { "type-fest": "^0.8.1" + }, + "dependencies": { + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + } } }, "has-flag": { @@ -6136,15 +7355,6 @@ } } }, - "eslint-config-prettier": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-6.11.0.tgz", - "integrity": "sha512-oB8cpLWSAjOVFEJhhyMZh6NOEOtBVziaqdDQ86+qhDHFbZXoRTM7pNSvFRfW/W/L/LrQ38C99J5CGuRBBzBsdA==", - "dev": true, - "requires": { - "get-stdin": "^6.0.0" - } - }, "eslint-config-semistandard": { "version": "15.0.1", "resolved": "https://registry.npmjs.org/eslint-config-semistandard/-/eslint-config-semistandard-15.0.1.tgz", @@ -11841,6 +13051,12 @@ "integrity": "sha512-0E4OIgBJVlAmf1KfYFtZ3gYxgUzC5Eb3Jzmrc9ikI1OY+/cM8Kh72Ti7KfpeHNeD3HJNf9SmEfmvQLIz44Hrhw==", "dev": true }, + "eslint-plugin-jest-formatting": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest-formatting/-/eslint-plugin-jest-formatting-2.0.1.tgz", + "integrity": "sha512-t6oc6GcXqzQ4lwatnVoKxGTLqo8kFIdA5kpaS3mrkwnTNxhsgFo9reMFdrtNtllx72ohNUsXsay7RJR/LLLk3Q==", + "dev": true + }, "eslint-plugin-json": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/eslint-plugin-json/-/eslint-plugin-json-1.4.0.tgz", @@ -11870,15 +13086,6 @@ } } }, - "eslint-plugin-prettier": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.1.4.tgz", - "integrity": "sha512-jZDa8z76klRqo+TdGDTFJSavwbnWK2ZpqGKNZ+VvweMW516pDUMmQ2koXvxEE4JhzNvTv+radye/bWGBmA6jmg==", - "dev": true, - "requires": { - "prettier-linter-helpers": "^1.0.0" - } - }, "eslint-plugin-promise": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-3.8.0.tgz", @@ -12076,26 +13283,25 @@ } }, "expect": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-25.5.0.tgz", - "integrity": "sha512-w7KAXo0+6qqZZhovCaBVPSIqQp7/UTcx4M9uKt2m6pd2VB1voyC8JizLRqeEqud3AAVP02g+hbErDu5gu64tlA==", + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/expect/-/expect-26.6.2.tgz", + "integrity": "sha512-9/hlOBkQl2l/PLHJx6JjoDF6xPKcJEsUlWKb23rKE7KzeDqUZKXKNMW27KIue5JMdBV9HgmoJPcc8HtO85t9IA==", "dev": true, "requires": { - "@jest/types": "^25.5.0", + "@jest/types": "^26.6.2", "ansi-styles": "^4.0.0", - "jest-get-type": "^25.2.6", - "jest-matcher-utils": "^25.5.0", - "jest-message-util": "^25.5.0", - "jest-regex-util": "^25.2.6" + "jest-get-type": "^26.3.0", + "jest-matcher-utils": "^26.6.2", + "jest-message-util": "^26.6.2", + "jest-regex-util": "^26.0.0" }, "dependencies": { "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, @@ -12113,6 +13319,12 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true + }, + "jest-regex-util": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-26.0.0.tgz", + "integrity": "sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A==", + "dev": true } } }, @@ -12231,12 +13443,6 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, - "fast-diff": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", - "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", - "dev": true - }, "fast-glob": { "version": "2.2.7", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.7.tgz", @@ -12263,6 +13469,15 @@ "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", "dev": true }, + "fastq": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.11.0.tgz", + "integrity": "sha512-7Eczs8gIPDrVzT+EksYBcupqMyxSHXXrHOLRRxU2/DicV8789MRBRR8+Hc2uWzUupOs4YS4JzBmBxjjCVBxD/g==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, "fb-watchman": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", @@ -12384,12 +13599,6 @@ "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==", "dev": true }, - "flow-bin": { - "version": "0.122.0", - "resolved": "https://registry.npmjs.org/flow-bin/-/flow-bin-0.122.0.tgz", - "integrity": "sha512-my8N5jgl/A+UVby9E7NDppHdhLgRbWgKbmFZSx2MSYMRh3d9YGnM2MM+wexpUpl0ftY1IM6ZcUwaAhrypLyvlA==", - "dev": true - }, "flush-write-stream": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", @@ -12554,9 +13763,9 @@ "dev": true }, "gensync": { - "version": "1.0.0-beta.1", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz", - "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==", + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true }, "get-caller-file": { @@ -12757,12 +13966,6 @@ "integrity": "sha512-/b3jarXkH8KJoOMQc3uVGHASwGLPq3gSFJ7tgJm2diza+bydJPTGOibin2steecKeOylE8oY2JERlVWkAJO6yw==", "dev": true }, - "get-stdin": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-6.0.0.tgz", - "integrity": "sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==", - "dev": true - }, "get-stream": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", @@ -13309,6 +14512,14 @@ "ignore": "^4.0.3", "pify": "^4.0.1", "slash": "^2.0.0" + }, + "dependencies": { + "slash": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", + "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", + "dev": true + } } }, "graceful-fs": { @@ -13442,12 +14653,12 @@ "dev": true }, "html-encoding-sniffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-1.0.2.tgz", - "integrity": "sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", + "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", "dev": true, "requires": { - "whatwg-encoding": "^1.0.1" + "whatwg-encoding": "^1.0.5" } }, "html-escaper": { @@ -13845,12 +15056,6 @@ "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", "dev": true }, - "ip-regex": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", - "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=", - "dev": true - }, "is-accessor-descriptor": { "version": "0.1.6", "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", @@ -13908,6 +15113,15 @@ "ci-info": "^2.0.0" } }, + "is-core-module": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz", + "integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, "is-data-descriptor": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", @@ -13960,9 +15174,9 @@ "dev": true }, "is-docker": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.0.0.tgz", - "integrity": "sha512-pJEdRugimx4fBMra5z2/5iRdZ63OhYV0vr0Dwm5+xtW4D1FvRkB8hamMIhnWfyJeDdyr/aa7BDyNbtG38VxgoQ==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.1.1.tgz", + "integrity": "sha512-ZOoqiXfEwtGknTiuDEy8pN2CfE3TxMHprvNer1mXiqwkOT77Rw3YVrUQ52EqAOU3QAWDQ+bQdx7HJzrv7LS2Hw==", "dev": true, "optional": true }, @@ -14046,6 +15260,12 @@ "isobject": "^3.0.1" } }, + "is-potential-custom-element-name": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz", + "integrity": "sha1-DFLlS8yjkbssSUsh6GJtczbG45c=", + "dev": true + }, "is-regex": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.0.tgz", @@ -14211,9 +15431,9 @@ "dev": true }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -14233,12 +15453,12 @@ }, "dependencies": { "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", "dev": true, "requires": { - "ms": "^2.1.1" + "ms": "2.1.2" } }, "ms": { @@ -14266,30 +15486,38 @@ } }, "jest": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/jest/-/jest-25.5.4.tgz", - "integrity": "sha512-hHFJROBTqZahnO+X+PMtT6G2/ztqAZJveGqz//FnWWHurizkD05PQGzRZOhF3XP6z7SJmL+5tCfW8qV06JypwQ==", + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/jest/-/jest-26.6.3.tgz", + "integrity": "sha512-lGS5PXGAzR4RF7V5+XObhqz2KZIDUA1yD0DG6pBVmy10eh0ZIXQImRuzocsI/N2XZ1GrLFwTS27In2i2jlpq1Q==", "dev": true, "requires": { - "@jest/core": "^25.5.4", + "@jest/core": "^26.6.3", "import-local": "^3.0.2", - "jest-cli": "^25.5.4" + "jest-cli": "^26.6.3" }, "dependencies": { "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -14311,53 +15539,100 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, "jest-cli": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-25.5.4.tgz", - "integrity": "sha512-rG8uJkIiOUpnREh1768/N3n27Cm+xPFkSNFO91tgg+8o2rXeVLStz+vkXkGr4UtzH6t1SNbjwoiswd7p4AhHTw==", + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-26.6.3.tgz", + "integrity": "sha512-GF9noBSa9t08pSyl3CY4frMrqp+aQXFGFkf5hEPbh/pIUFYWMK6ZLTfbmadxJVcJrdRoChlWQsA2VkJcDFK8hg==", "dev": true, "requires": { - "@jest/core": "^25.5.4", - "@jest/test-result": "^25.5.0", - "@jest/types": "^25.5.0", - "chalk": "^3.0.0", + "@jest/core": "^26.6.3", + "@jest/test-result": "^26.6.2", + "@jest/types": "^26.6.2", + "chalk": "^4.0.0", "exit": "^0.1.2", "graceful-fs": "^4.2.4", "import-local": "^3.0.2", "is-ci": "^2.0.0", - "jest-config": "^25.5.4", - "jest-util": "^25.5.0", - "jest-validate": "^25.5.0", + "jest-config": "^26.6.3", + "jest-util": "^26.6.2", + "jest-validate": "^26.6.2", "prompts": "^2.0.1", - "realpath-native": "^2.0.0", - "yargs": "^15.3.1" + "yargs": "^15.4.1" + } + }, + "jest-util": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", + "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + } + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" } }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } } } }, "jest-changed-files": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-25.5.0.tgz", - "integrity": "sha512-EOw9QEqapsDT7mKF162m8HFzRPbmP8qJQny6ldVOdOVBz3ACgPm/1nAn5fPQ/NDaYhX/AHkrGwwkCncpAVSXcw==", + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-26.6.2.tgz", + "integrity": "sha512-fDS7szLcY9sCtIip8Fjry9oGf3I2ht/QT21bAHm5Dmf0mD4X3ReNUf17y+bO6fR8WgbIZTlbyG1ak/53cbRzKQ==", "dev": true, "requires": { - "@jest/types": "^25.5.0", - "execa": "^3.2.0", + "@jest/types": "^26.6.2", + "execa": "^4.0.0", "throat": "^5.0.0" }, "dependencies": { @@ -14373,9 +15648,9 @@ } }, "execa": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-3.4.0.tgz", - "integrity": "sha512-r9vdGQk4bmCuK1yKQu1KTwcT2zwfWdbdaXfCtAh+5nU/4fSX+JAb7vZGvI5naJrQlvONrEB20jeruESI69530g==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", + "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", "dev": true, "requires": { "cross-spawn": "^7.0.0", @@ -14385,15 +15660,14 @@ "merge-stream": "^2.0.0", "npm-run-path": "^4.0.0", "onetime": "^5.1.0", - "p-finally": "^2.0.0", "signal-exit": "^3.0.2", "strip-final-newline": "^2.0.0" } }, "get-stream": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", - "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "dev": true, "requires": { "pump": "^3.0.0" @@ -14414,12 +15688,6 @@ "path-key": "^3.0.0" } }, - "p-finally": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz", - "integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==", - "dev": true - }, "path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -14444,42 +15712,131 @@ } }, "jest-config": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-25.5.4.tgz", - "integrity": "sha512-SZwR91SwcdK6bz7Gco8qL7YY2sx8tFJYzvg216DLihTWf+LKY/DoJXpM9nTzYakSyfblbqeU48p/p7Jzy05Atg==", + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-26.6.3.tgz", + "integrity": "sha512-t5qdIj/bCj2j7NFVHb2nFB4aUdfucDn3JRKgrZnplb8nieAirAzRSHP8uDEd+qV6ygzg9Pz4YG7UTJf94LPSyg==", "dev": true, "requires": { "@babel/core": "^7.1.0", - "@jest/test-sequencer": "^25.5.4", - "@jest/types": "^25.5.0", - "babel-jest": "^25.5.1", - "chalk": "^3.0.0", + "@jest/test-sequencer": "^26.6.3", + "@jest/types": "^26.6.2", + "babel-jest": "^26.6.3", + "chalk": "^4.0.0", "deepmerge": "^4.2.2", "glob": "^7.1.1", "graceful-fs": "^4.2.4", - "jest-environment-jsdom": "^25.5.0", - "jest-environment-node": "^25.5.0", - "jest-get-type": "^25.2.6", - "jest-jasmine2": "^25.5.4", - "jest-regex-util": "^25.2.6", - "jest-resolve": "^25.5.1", - "jest-util": "^25.5.0", - "jest-validate": "^25.5.0", + "jest-environment-jsdom": "^26.6.2", + "jest-environment-node": "^26.6.2", + "jest-get-type": "^26.3.0", + "jest-jasmine2": "^26.6.3", + "jest-regex-util": "^26.0.0", + "jest-resolve": "^26.6.2", + "jest-util": "^26.6.2", + "jest-validate": "^26.6.2", "micromatch": "^4.0.2", - "pretty-format": "^25.5.0", - "realpath-native": "^2.0.0" + "pretty-format": "^26.6.2" }, "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "@jest/transform": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-26.6.2.tgz", + "integrity": "sha512-E9JjhUgNzvuQ+vVAL21vlyfy12gP0GhazGgJC4h6qUt1jSdUXGWJ1wfu/X7Sd8etSgxV4ovT1pb9v5D6QW4XgA==", + "dev": true, + "requires": { + "@babel/core": "^7.1.0", + "@jest/types": "^26.6.2", + "babel-plugin-istanbul": "^6.0.0", + "chalk": "^4.0.0", + "convert-source-map": "^1.4.0", + "fast-json-stable-stringify": "^2.0.0", + "graceful-fs": "^4.2.4", + "jest-haste-map": "^26.6.2", + "jest-regex-util": "^26.0.0", + "jest-util": "^26.6.2", + "micromatch": "^4.0.2", + "pirates": "^4.0.1", + "slash": "^3.0.0", + "source-map": "^0.6.1", + "write-file-atomic": "^3.0.0" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "babel-jest": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-26.6.3.tgz", + "integrity": "sha512-pl4Q+GAVOHwvjrck6jKjvmGhnO3jHX/xuB9d27f+EJZ/6k+6nMuPjorrYp7s++bKKdANwzElBWnLWaObvTnaZA==", + "dev": true, + "requires": { + "@jest/transform": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/babel__core": "^7.1.7", + "babel-plugin-istanbul": "^6.0.0", + "babel-preset-jest": "^26.6.2", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "slash": "^3.0.0" + } + }, + "babel-plugin-jest-hoist": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.6.2.tgz", + "integrity": "sha512-PO9t0697lNTmcEHH69mdtYiOIkkOlj9fySqfO3K1eCcdISevLAE0xY59VLLUj0SoiPiTX/JU2CYFpILydUa5Lw==", + "dev": true, + "requires": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.0.0", + "@types/babel__traverse": "^7.0.6" + } + }, + "babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "requires": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + } + }, + "babel-preset-jest": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-26.6.2.tgz", + "integrity": "sha512-YvdtlVm9t3k777c5NPQIv6cxFFFapys25HiUmuSgHwIZhfifweR5c5Sf5nwE3MAbfu327CYSvps8Yx6ANLyleQ==", + "dev": true, + "requires": { + "babel-plugin-jest-hoist": "^26.6.2", + "babel-preset-current-node-syntax": "^1.0.0" + } + }, "braces": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", @@ -14490,9 +15847,9 @@ } }, "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -14523,6 +15880,13 @@ "to-regex-range": "^5.0.1" } }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -14535,6 +15899,69 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, + "jest-haste-map": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-26.6.2.tgz", + "integrity": "sha512-easWIJXIw71B2RdR8kgqpjQrbMRWQBgiBwXYEhtGUTaX+doCjBheluShdDMeR8IMfJiTqH4+zfhtg29apJf/8w==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "@types/graceful-fs": "^4.1.2", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "fsevents": "^2.1.2", + "graceful-fs": "^4.2.4", + "jest-regex-util": "^26.0.0", + "jest-serializer": "^26.6.2", + "jest-util": "^26.6.2", + "jest-worker": "^26.6.2", + "micromatch": "^4.0.2", + "sane": "^4.0.3", + "walker": "^1.0.7" + } + }, + "jest-regex-util": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-26.0.0.tgz", + "integrity": "sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A==", + "dev": true + }, + "jest-serializer": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-26.6.2.tgz", + "integrity": "sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g==", + "dev": true, + "requires": { + "@types/node": "*", + "graceful-fs": "^4.2.4" + } + }, + "jest-util": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", + "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + } + }, + "jest-worker": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", + "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", + "dev": true, + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + } + }, "micromatch": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", @@ -14545,10 +15972,22 @@ "picomatch": "^2.0.5" } }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -14566,31 +16005,30 @@ } }, "jest-diff": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-25.5.0.tgz", - "integrity": "sha512-z1kygetuPiREYdNIumRpAHY6RXiGmp70YHptjdaxTWGmA085W3iCnXNx0DhflK3vwrKmrRWyY1wUpkPMVxMK7A==", + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", + "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", "dev": true, "requires": { - "chalk": "^3.0.0", - "diff-sequences": "^25.2.6", - "jest-get-type": "^25.2.6", - "pretty-format": "^25.5.0" + "chalk": "^4.0.0", + "diff-sequences": "^26.6.2", + "jest-get-type": "^26.3.0", + "pretty-format": "^26.6.2" }, "dependencies": { "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -14619,9 +16057,9 @@ "dev": true }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -14630,152 +16068,36 @@ } }, "jest-docblock": { - "version": "25.3.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-25.3.0.tgz", - "integrity": "sha512-aktF0kCar8+zxRHxQZwxMy70stc9R1mOmrLsT5VO3pIT0uzGRSDAXxSlz4NqQWpuLjPpuMhPRl7H+5FRsvIQAg==", + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-26.0.0.tgz", + "integrity": "sha512-RDZ4Iz3QbtRWycd8bUEPxQsTlYazfYn/h5R65Fc6gOfwozFhoImx+affzky/FFBuqISPTqjXomoIGJVKBWoo0w==", "dev": true, "requires": { "detect-newline": "^3.0.0" } }, "jest-each": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-25.5.0.tgz", - "integrity": "sha512-QBogUxna3D8vtiItvn54xXde7+vuzqRrEeaw8r1s+1TG9eZLVJE5ZkKoSUlqFwRjnlaA4hyKGiu9OlkFIuKnjA==", + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-26.6.2.tgz", + "integrity": "sha512-Mer/f0KaATbjl8MCJ+0GEpNdqmnVmDYqCTJYTvoo7rqmRiDllmp2AYN+06F93nXcY3ur9ShIjS+CO/uD+BbH4A==", "dev": true, "requires": { - "@jest/types": "^25.5.0", - "chalk": "^3.0.0", - "jest-get-type": "^25.2.6", - "jest-util": "^25.5.0", - "pretty-format": "^25.5.0" + "@jest/types": "^26.6.2", + "chalk": "^4.0.0", + "jest-get-type": "^26.3.0", + "jest-util": "^26.6.2", + "pretty-format": "^26.6.2" }, "dependencies": { "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-environment-jsdom": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-25.5.0.tgz", - "integrity": "sha512-7Jr02ydaq4jaWMZLY+Skn8wL5nVIYpWvmeatOHL3tOcV3Zw8sjnPpx+ZdeBfc457p8jCR9J6YCc+Lga0oIy62A==", - "dev": true, - "requires": { - "@jest/environment": "^25.5.0", - "@jest/fake-timers": "^25.5.0", - "@jest/types": "^25.5.0", - "jest-mock": "^25.5.0", - "jest-util": "^25.5.0", - "jsdom": "^15.2.1" - } - }, - "jest-environment-node": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-25.5.0.tgz", - "integrity": "sha512-iuxK6rQR2En9EID+2k+IBs5fCFd919gVVK5BeND82fYeLWPqvRcFNPKu9+gxTwfB5XwBGBvZ0HFQa+cHtIoslA==", - "dev": true, - "requires": { - "@jest/environment": "^25.5.0", - "@jest/fake-timers": "^25.5.0", - "@jest/types": "^25.5.0", - "jest-mock": "^25.5.0", - "jest-util": "^25.5.0", - "semver": "^6.3.0" - }, - "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } - } - }, - "jest-get-type": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-25.2.6.tgz", - "integrity": "sha512-DxjtyzOHjObRM+sM1knti6or+eOgcGU4xVSb2HNP1TqO4ahsT+rqZg+nyqHWJSvWgKC5cG3QjGFBqxLghiF/Ig==", - "dev": true - }, - "jest-haste-map": { - "version": "25.5.1", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-25.5.1.tgz", - "integrity": "sha512-dddgh9UZjV7SCDQUrQ+5t9yy8iEgKc1AKqZR9YDww8xsVOtzPQSMVLDChc21+g29oTRexb9/B0bIlZL+sWmvAQ==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "@types/graceful-fs": "^4.1.2", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "fsevents": "^2.1.2", - "graceful-fs": "^4.2.4", - "jest-serializer": "^25.5.0", - "jest-util": "^25.5.0", - "jest-worker": "^25.5.0", - "micromatch": "^4.0.2", - "sane": "^4.0.3", - "walker": "^1.0.7", - "which": "^2.0.2" - }, - "dependencies": { - "anymatch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", - "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, "braces": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", @@ -14785,6 +16107,31 @@ "fill-range": "^7.0.1" } }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -14794,12 +16141,11 @@ "to-regex-range": "^5.0.1" } }, - "fsevents": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.1.3.tgz", - "integrity": "sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ==", - "dev": true, - "optional": true + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true }, "is-number": { "version": "7.0.0", @@ -14807,6 +16153,20 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true }, + "jest-util": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", + "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + } + }, "micromatch": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", @@ -14817,6 +16177,15 @@ "picomatch": "^2.0.5" } }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -14828,180 +16197,27 @@ } } }, - "jest-jasmine2": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-25.5.4.tgz", - "integrity": "sha512-9acbWEfbmS8UpdcfqnDO+uBUgKa/9hcRh983IHdM+pKmJPL77G0sWAAK0V0kr5LK3a8cSBfkFSoncXwQlRZfkQ==", + "jest-environment-jsdom": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-26.6.2.tgz", + "integrity": "sha512-jgPqCruTlt3Kwqg5/WVFyHIOJHsiAvhcp2qiR2QQstuG9yWox5+iHpU3ZrcBxW14T4fe5Z68jAfLRh7joCSP2Q==", "dev": true, "requires": { - "@babel/traverse": "^7.1.0", - "@jest/environment": "^25.5.0", - "@jest/source-map": "^25.5.0", - "@jest/test-result": "^25.5.0", - "@jest/types": "^25.5.0", - "chalk": "^3.0.0", - "co": "^4.6.0", - "expect": "^25.5.0", - "is-generator-fn": "^2.0.0", - "jest-each": "^25.5.0", - "jest-matcher-utils": "^25.5.0", - "jest-message-util": "^25.5.0", - "jest-runtime": "^25.5.4", - "jest-snapshot": "^25.5.1", - "jest-util": "^25.5.0", - "pretty-format": "^25.5.0", - "throat": "^5.0.0" + "@jest/environment": "^26.6.2", + "@jest/fake-timers": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "jest-mock": "^26.6.2", + "jest-util": "^26.6.2", + "jsdom": "^16.4.0" }, "dependencies": { "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-leak-detector": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-25.5.0.tgz", - "integrity": "sha512-rV7JdLsanS8OkdDpZtgBf61L5xZ4NnYLBq72r6ldxahJWWczZjXawRsoHyXzibM5ed7C2QRjpp6ypgwGdKyoVA==", - "dev": true, - "requires": { - "jest-get-type": "^25.2.6", - "pretty-format": "^25.5.0" - } - }, - "jest-matcher-utils": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-25.5.0.tgz", - "integrity": "sha512-VWI269+9JS5cpndnpCwm7dy7JtGQT30UHfrnM3mXl22gHGt/b7NkjBqXfbhZ8V4B7ANUsjK18PlSBmG0YH7gjw==", - "dev": true, - "requires": { - "chalk": "^3.0.0", - "jest-diff": "^25.5.0", - "jest-get-type": "^25.2.6", - "pretty-format": "^25.5.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-message-util": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-25.5.0.tgz", - "integrity": "sha512-ezddz3YCT/LT0SKAmylVyWWIGYoKHOFOFXx3/nA4m794lfVUskMcwhip6vTgdVrOtYdjeQeis2ypzes9mZb4EA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@jest/types": "^25.5.0", - "@types/stack-utils": "^1.0.1", - "chalk": "^3.0.0", - "graceful-fs": "^4.2.4", - "micromatch": "^4.0.2", - "slash": "^3.0.0", - "stack-utils": "^1.0.1" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, @@ -15015,9 +16231,469 @@ } }, "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "jest-util": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", + "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + } + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "jest-environment-node": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-26.6.2.tgz", + "integrity": "sha512-zhtMio3Exty18dy8ee8eJ9kjnRyZC1N4C1Nt/VShN1apyXc8rWGtJ9lI7vqiWcyyXS4BVSEn9lxAM2D+07/Tag==", + "dev": true, + "requires": { + "@jest/environment": "^26.6.2", + "@jest/fake-timers": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "jest-mock": "^26.6.2", + "jest-util": "^26.6.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "jest-util": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", + "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + } + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "jest-get-type": { + "version": "26.3.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", + "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", + "dev": true + }, + "jest-jasmine2": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-26.6.3.tgz", + "integrity": "sha512-kPKUrQtc8aYwBV7CqBg5pu+tmYXlvFlSFYn18ev4gPFtrRzB15N2gW/Roew3187q2w2eHuu0MU9TJz6w0/nPEg==", + "dev": true, + "requires": { + "@babel/traverse": "^7.1.0", + "@jest/environment": "^26.6.2", + "@jest/source-map": "^26.6.2", + "@jest/test-result": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "expect": "^26.6.2", + "is-generator-fn": "^2.0.0", + "jest-each": "^26.6.2", + "jest-matcher-utils": "^26.6.2", + "jest-message-util": "^26.6.2", + "jest-runtime": "^26.6.3", + "jest-snapshot": "^26.6.2", + "jest-util": "^26.6.2", + "pretty-format": "^26.6.2", + "throat": "^5.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "jest-util": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", + "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + } + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + } + } + }, + "jest-leak-detector": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-26.6.2.tgz", + "integrity": "sha512-i4xlXpsVSMeKvg2cEKdfhh0H39qlJlP5Ex1yQxwF9ubahboQYMgTtz5oML35AVA3B4Eu+YsmwaiKVev9KCvLxg==", + "dev": true, + "requires": { + "jest-get-type": "^26.3.0", + "pretty-format": "^26.6.2" + } + }, + "jest-matcher-utils": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-26.6.2.tgz", + "integrity": "sha512-llnc8vQgYcNqDrqRDXWwMr9i7rS5XFiCwvh6DTP7Jqa2mqpcCBBlpCbn+trkG0KNhPu/h8rzyBkriOtBstvWhw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^26.6.2", + "jest-get-type": "^26.3.0", + "pretty-format": "^26.6.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jest-message-util": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-26.6.2.tgz", + "integrity": "sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@jest/types": "^26.6.2", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "micromatch": "^4.0.2", + "pretty-format": "^26.6.2", + "slash": "^3.0.0", + "stack-utils": "^2.0.2" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chalk": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -15077,9 +16753,9 @@ "dev": true }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -15097,12 +16773,13 @@ } }, "jest-mock": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-25.5.0.tgz", - "integrity": "sha512-eXWuTV8mKzp/ovHc5+3USJMYsTBhyQ+5A1Mak35dey/RG8GlM4YWVylZuGgVXinaW6tpvk/RSecmF37FKUlpXA==", + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-26.6.2.tgz", + "integrity": "sha512-YyFjePHHp1LzpzYcmgqkJ0nm0gg/lJx2aZFzFy1S6eUqNjXsOqTK10zNRff2dNfssgokjkG65OlWNcIlgd3zew==", "dev": true, "requires": { - "@jest/types": "^25.5.0" + "@jest/types": "^26.6.2", + "@types/node": "*" } }, "jest-pnp-resolver": { @@ -15111,43 +16788,44 @@ "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", "dev": true }, - "jest-regex-util": { - "version": "25.2.6", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-25.2.6.tgz", - "integrity": "sha512-KQqf7a0NrtCkYmZZzodPftn7fL1cq3GQAFVMn5Hg8uKx/fIenLEobNanUxb7abQ1sjADHBseG/2FGpsv/wr+Qw==", - "dev": true - }, "jest-resolve": { - "version": "25.5.1", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-25.5.1.tgz", - "integrity": "sha512-Hc09hYch5aWdtejsUZhA+vSzcotf7fajSlPA6EZPE1RmPBAD39XtJhvHWFStid58iit4IPDLI/Da4cwdDmAHiQ==", + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-26.6.2.tgz", + "integrity": "sha512-sOxsZOq25mT1wRsfHcbtkInS+Ek7Q8jCHUB0ZUTP0tc/c41QHriU/NunqMfCUWsL4H3MHpvQD4QR9kSYhS7UvQ==", "dev": true, "requires": { - "@jest/types": "^25.5.0", - "browser-resolve": "^1.11.3", - "chalk": "^3.0.0", + "@jest/types": "^26.6.2", + "chalk": "^4.0.0", "graceful-fs": "^4.2.4", - "jest-pnp-resolver": "^1.2.1", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^26.6.2", "read-pkg-up": "^7.0.1", - "realpath-native": "^2.0.0", - "resolve": "^1.17.0", + "resolve": "^1.18.1", "slash": "^3.0.0" }, "dependencies": { "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -15169,21 +16847,60 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "jest-util": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", + "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + } + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, "parse-json": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.0.1.tgz", - "integrity": "sha512-ztoZ4/DYeXQq4E21v169sC8qWINGpcosGv9XhTDvg9/hWvx/zrFkc9BiWxR58OJLHGk28j5BL0SDLeV2WmFZlQ==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "dev": true, "requires": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1", + "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, @@ -15216,6 +16933,24 @@ "find-up": "^4.1.0", "read-pkg": "^5.2.0", "type-fest": "^0.8.1" + }, + "dependencies": { + "type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true + } + } + }, + "resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dev": true, + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" } }, "slash": { @@ -15225,68 +16960,104 @@ "dev": true }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } } } }, "jest-resolve-dependencies": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-25.5.4.tgz", - "integrity": "sha512-yFmbPd+DAQjJQg88HveObcGBA32nqNZ02fjYmtL16t1xw9bAttSn5UGRRhzMHIQbsep7znWvAvnD4kDqOFM0Uw==", + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-26.6.3.tgz", + "integrity": "sha512-pVwUjJkxbhe4RY8QEWzN3vns2kqyuldKpxlxJlzEYfKSvY6/bMvxoFrYYzUO1Gx28yKWN37qyV7rIoIp2h8fTg==", "dev": true, "requires": { - "@jest/types": "^25.5.0", - "jest-regex-util": "^25.2.6", - "jest-snapshot": "^25.5.1" + "@jest/types": "^26.6.2", + "jest-regex-util": "^26.0.0", + "jest-snapshot": "^26.6.2" + }, + "dependencies": { + "jest-regex-util": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-26.0.0.tgz", + "integrity": "sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A==", + "dev": true + } } }, "jest-runner": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-25.5.4.tgz", - "integrity": "sha512-V/2R7fKZo6blP8E9BL9vJ8aTU4TH2beuqGNxHbxi6t14XzTb+x90B3FRgdvuHm41GY8ch4xxvf0ATH4hdpjTqg==", + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-26.6.3.tgz", + "integrity": "sha512-atgKpRHnaA2OvByG/HpGA4g6CSPS/1LK0jK3gATJAoptC1ojltpmVlYC3TYgdmGp+GLuhzpH30Gvs36szSL2JQ==", "dev": true, "requires": { - "@jest/console": "^25.5.0", - "@jest/environment": "^25.5.0", - "@jest/test-result": "^25.5.0", - "@jest/types": "^25.5.0", - "chalk": "^3.0.0", + "@jest/console": "^26.6.2", + "@jest/environment": "^26.6.2", + "@jest/test-result": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.7.1", "exit": "^0.1.2", "graceful-fs": "^4.2.4", - "jest-config": "^25.5.4", - "jest-docblock": "^25.3.0", - "jest-haste-map": "^25.5.1", - "jest-jasmine2": "^25.5.4", - "jest-leak-detector": "^25.5.0", - "jest-message-util": "^25.5.0", - "jest-resolve": "^25.5.1", - "jest-runtime": "^25.5.4", - "jest-util": "^25.5.0", - "jest-worker": "^25.5.0", + "jest-config": "^26.6.3", + "jest-docblock": "^26.0.0", + "jest-haste-map": "^26.6.2", + "jest-leak-detector": "^26.6.2", + "jest-message-util": "^26.6.2", + "jest-resolve": "^26.6.2", + "jest-runtime": "^26.6.3", + "jest-util": "^26.6.2", + "jest-worker": "^26.6.2", "source-map-support": "^0.5.6", "throat": "^5.0.0" }, "dependencies": { "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -15308,71 +17079,217 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "jest-haste-map": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-26.6.2.tgz", + "integrity": "sha512-easWIJXIw71B2RdR8kgqpjQrbMRWQBgiBwXYEhtGUTaX+doCjBheluShdDMeR8IMfJiTqH4+zfhtg29apJf/8w==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "@types/graceful-fs": "^4.1.2", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "fsevents": "^2.1.2", + "graceful-fs": "^4.2.4", + "jest-regex-util": "^26.0.0", + "jest-serializer": "^26.6.2", + "jest-util": "^26.6.2", + "jest-worker": "^26.6.2", + "micromatch": "^4.0.2", + "sane": "^4.0.3", + "walker": "^1.0.7" + } + }, + "jest-regex-util": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-26.0.0.tgz", + "integrity": "sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A==", + "dev": true + }, + "jest-serializer": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-26.6.2.tgz", + "integrity": "sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g==", + "dev": true, + "requires": { + "@types/node": "*", + "graceful-fs": "^4.2.4" + } + }, + "jest-util": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", + "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + } + }, + "jest-worker": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", + "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", + "dev": true, + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + } + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } } } }, "jest-runtime": { - "version": "25.5.4", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-25.5.4.tgz", - "integrity": "sha512-RWTt8LeWh3GvjYtASH2eezkc8AehVoWKK20udV6n3/gC87wlTbE1kIA+opCvNWyyPeBs6ptYsc6nyHUb1GlUVQ==", + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-26.6.3.tgz", + "integrity": "sha512-lrzyR3N8sacTAMeonbqpnSka1dHNux2uk0qqDXVkMv2c/A3wYnvQ4EXuI013Y6+gSKSCxdaczvf4HF0mVXHRdw==", "dev": true, "requires": { - "@jest/console": "^25.5.0", - "@jest/environment": "^25.5.0", - "@jest/globals": "^25.5.2", - "@jest/source-map": "^25.5.0", - "@jest/test-result": "^25.5.0", - "@jest/transform": "^25.5.1", - "@jest/types": "^25.5.0", + "@jest/console": "^26.6.2", + "@jest/environment": "^26.6.2", + "@jest/fake-timers": "^26.6.2", + "@jest/globals": "^26.6.2", + "@jest/source-map": "^26.6.2", + "@jest/test-result": "^26.6.2", + "@jest/transform": "^26.6.2", + "@jest/types": "^26.6.2", "@types/yargs": "^15.0.0", - "chalk": "^3.0.0", + "chalk": "^4.0.0", + "cjs-module-lexer": "^0.6.0", "collect-v8-coverage": "^1.0.0", "exit": "^0.1.2", "glob": "^7.1.3", "graceful-fs": "^4.2.4", - "jest-config": "^25.5.4", - "jest-haste-map": "^25.5.1", - "jest-message-util": "^25.5.0", - "jest-mock": "^25.5.0", - "jest-regex-util": "^25.2.6", - "jest-resolve": "^25.5.1", - "jest-snapshot": "^25.5.1", - "jest-util": "^25.5.0", - "jest-validate": "^25.5.0", - "realpath-native": "^2.0.0", + "jest-config": "^26.6.3", + "jest-haste-map": "^26.6.2", + "jest-message-util": "^26.6.2", + "jest-mock": "^26.6.2", + "jest-regex-util": "^26.0.0", + "jest-resolve": "^26.6.2", + "jest-snapshot": "^26.6.2", + "jest-util": "^26.6.2", + "jest-validate": "^26.6.2", "slash": "^3.0.0", "strip-bom": "^4.0.0", - "yargs": "^15.3.1" + "yargs": "^15.4.1" }, "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "@jest/transform": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-26.6.2.tgz", + "integrity": "sha512-E9JjhUgNzvuQ+vVAL21vlyfy12gP0GhazGgJC4h6qUt1jSdUXGWJ1wfu/X7Sd8etSgxV4ovT1pb9v5D6QW4XgA==", + "dev": true, + "requires": { + "@babel/core": "^7.1.0", + "@jest/types": "^26.6.2", + "babel-plugin-istanbul": "^6.0.0", + "chalk": "^4.0.0", + "convert-source-map": "^1.4.0", + "fast-json-stable-stringify": "^2.0.0", + "graceful-fs": "^4.2.4", + "jest-haste-map": "^26.6.2", + "jest-regex-util": "^26.0.0", + "jest-util": "^26.6.2", + "micromatch": "^4.0.2", + "pirates": "^4.0.1", + "slash": "^3.0.0", + "source-map": "^0.6.1", + "write-file-atomic": "^3.0.0" + } + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -15394,18 +17311,119 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "jest-haste-map": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-26.6.2.tgz", + "integrity": "sha512-easWIJXIw71B2RdR8kgqpjQrbMRWQBgiBwXYEhtGUTaX+doCjBheluShdDMeR8IMfJiTqH4+zfhtg29apJf/8w==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "@types/graceful-fs": "^4.1.2", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "fsevents": "^2.1.2", + "graceful-fs": "^4.2.4", + "jest-regex-util": "^26.0.0", + "jest-serializer": "^26.6.2", + "jest-util": "^26.6.2", + "jest-worker": "^26.6.2", + "micromatch": "^4.0.2", + "sane": "^4.0.3", + "walker": "^1.0.7" + } + }, + "jest-regex-util": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-26.0.0.tgz", + "integrity": "sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A==", + "dev": true + }, + "jest-serializer": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-26.6.2.tgz", + "integrity": "sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g==", + "dev": true, + "requires": { + "@types/node": "*", + "graceful-fs": "^4.2.4" + } + }, + "jest-util": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", + "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + } + }, + "jest-worker": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", + "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", + "dev": true, + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + } + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, "strip-bom": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", @@ -15413,62 +17431,81 @@ "dev": true }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } } } }, - "jest-serializer": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-25.5.0.tgz", - "integrity": "sha512-LxD8fY1lByomEPflwur9o4e2a5twSQ7TaVNLlFUuToIdoJuBt8tzHfCsZ42Ok6LkKXWzFWf3AGmheuLAA7LcCA==", - "dev": true, - "requires": { - "graceful-fs": "^4.2.4" - } - }, "jest-snapshot": { - "version": "25.5.1", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-25.5.1.tgz", - "integrity": "sha512-C02JE1TUe64p2v1auUJ2ze5vcuv32tkv9PyhEb318e8XOKF7MOyXdJ7kdjbvrp3ChPLU2usI7Rjxs97Dj5P0uQ==", + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-26.6.2.tgz", + "integrity": "sha512-OLhxz05EzUtsAmOMzuupt1lHYXCNib0ECyuZ/PZOx9TrZcC8vL0x+DUG3TL+GLX3yHG45e6YGjIm0XwDc3q3og==", "dev": true, "requires": { "@babel/types": "^7.0.0", - "@jest/types": "^25.5.0", - "@types/prettier": "^1.19.0", - "chalk": "^3.0.0", - "expect": "^25.5.0", + "@jest/types": "^26.6.2", + "@types/babel__traverse": "^7.0.4", + "@types/prettier": "^2.0.0", + "chalk": "^4.0.0", + "expect": "^26.6.2", "graceful-fs": "^4.2.4", - "jest-diff": "^25.5.0", - "jest-get-type": "^25.2.6", - "jest-matcher-utils": "^25.5.0", - "jest-message-util": "^25.5.0", - "jest-resolve": "^25.5.1", - "make-dir": "^3.0.0", + "jest-diff": "^26.6.2", + "jest-get-type": "^26.3.0", + "jest-haste-map": "^26.6.2", + "jest-matcher-utils": "^26.6.2", + "jest-message-util": "^26.6.2", + "jest-resolve": "^26.6.2", "natural-compare": "^1.4.0", - "pretty-format": "^25.5.0", - "semver": "^6.3.0" + "pretty-format": "^26.6.2", + "semver": "^7.3.2" }, "dependencies": { "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, + "anymatch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", + "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -15490,85 +17527,21 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "dev": true, "requires": { - "semver": "^6.0.0" + "to-regex-range": "^5.0.1" } }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "jest-util": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-25.5.0.tgz", - "integrity": "sha512-KVlX+WWg1zUTB9ktvhsg2PXZVdkI1NBevOJSkTKYAyXyH4QSvh+Lay/e/v+bmaFfrkfx43xD8QTfgobzlEXdIA==", - "dev": true, - "requires": { - "@jest/types": "^25.5.0", - "chalk": "^3.0.0", - "graceful-fs": "^4.2.4", - "is-ci": "^2.0.0", - "make-dir": "^3.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", - "dev": true, - "requires": { - "@types/color-name": "^1.1.1", - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "optional": true }, "has-flag": { "version": "4.0.0", @@ -15576,60 +17549,162 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, - "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "jest-haste-map": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-26.6.2.tgz", + "integrity": "sha512-easWIJXIw71B2RdR8kgqpjQrbMRWQBgiBwXYEhtGUTaX+doCjBheluShdDMeR8IMfJiTqH4+zfhtg29apJf/8w==", "dev": true, "requires": { - "semver": "^6.0.0" + "@jest/types": "^26.6.2", + "@types/graceful-fs": "^4.1.2", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "fsevents": "^2.1.2", + "graceful-fs": "^4.2.4", + "jest-regex-util": "^26.0.0", + "jest-serializer": "^26.6.2", + "jest-util": "^26.6.2", + "jest-worker": "^26.6.2", + "micromatch": "^4.0.2", + "sane": "^4.0.3", + "walker": "^1.0.7" + } + }, + "jest-regex-util": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-26.0.0.tgz", + "integrity": "sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A==", + "dev": true + }, + "jest-serializer": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-26.6.2.tgz", + "integrity": "sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g==", + "dev": true, + "requires": { + "@types/node": "*", + "graceful-fs": "^4.2.4" + } + }, + "jest-util": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", + "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + } + }, + "jest-worker": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", + "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", + "dev": true, + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^7.0.0" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" } }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true } } }, "jest-validate": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-25.5.0.tgz", - "integrity": "sha512-okUFKqhZIpo3jDdtUXUZ2LxGUZJIlfdYBvZb1aczzxrlyMlqdnnws9MOxezoLGhSaFc2XYaHNReNQfj5zPIWyQ==", + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-26.6.2.tgz", + "integrity": "sha512-NEYZ9Aeyj0i5rQqbq+tpIOom0YS1u2MVu6+euBsvpgIme+FOfRmoC4R5p0JiAUpaFvFy24xgrpMknarR/93XjQ==", "dev": true, "requires": { - "@jest/types": "^25.5.0", - "camelcase": "^5.3.1", - "chalk": "^3.0.0", - "jest-get-type": "^25.2.6", + "@jest/types": "^26.6.2", + "camelcase": "^6.0.0", + "chalk": "^4.0.0", + "jest-get-type": "^26.3.0", "leven": "^3.1.0", - "pretty-format": "^25.5.0" + "pretty-format": "^26.6.2" }, "dependencies": { "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, + "camelcase": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", + "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", + "dev": true + }, "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -15658,9 +17733,9 @@ "dev": true }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -15669,33 +17744,42 @@ } }, "jest-watcher": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-25.5.0.tgz", - "integrity": "sha512-XrSfJnVASEl+5+bb51V0Q7WQx65dTSk7NL4yDdVjPnRNpM0hG+ncFmDYJo9O8jaSRcAitVbuVawyXCRoxGrT5Q==", + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-26.6.2.tgz", + "integrity": "sha512-WKJob0P/Em2csiVthsI68p6aGKTIcsfjH9Gsx1f0A3Italz43e3ho0geSAVsmj09RWOELP1AZ/DXyJgOgDKxXQ==", "dev": true, "requires": { - "@jest/test-result": "^25.5.0", - "@jest/types": "^25.5.0", + "@jest/test-result": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", "ansi-escapes": "^4.2.1", - "chalk": "^3.0.0", - "jest-util": "^25.5.0", - "string-length": "^3.1.0" + "chalk": "^4.0.0", + "jest-util": "^26.6.2", + "string-length": "^4.0.1" }, "dependencies": { "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.0.tgz", + "integrity": "sha512-qwx12AxXe2Q5xQ43Ac//I6v5aXTipYrSESdOgzrN+9XjgEpyjpKuvSGaN4qE93f7TQTlerQQ8S+EQ0EyDoVL1A==", "dev": true, "requires": { "ansi-styles": "^4.1.0", @@ -15717,46 +17801,67 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, "has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "jest-util": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", + "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + } + }, + "micromatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.2.tgz", + "integrity": "sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.0.5" + } + }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" } - } - } - }, - "jest-worker": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-25.5.0.tgz", - "integrity": "sha512-/dsSmUkIy5EBGfv/IjjqmFxrNAUpBERfGs1oHROyD7yxjG/w+t0GOJDX8O1k32ySmd7+a5IhnJU2qQFcJ4n1vw==", - "dev": true, - "requires": { - "merge-stream": "^2.0.0", - "supports-color": "^7.0.0" - }, - "dependencies": { - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true }, - "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, "requires": { - "has-flag": "^4.0.0" + "is-number": "^7.0.0" } } } @@ -15784,37 +17889,71 @@ "dev": true }, "jsdom": { - "version": "15.2.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-15.2.1.tgz", - "integrity": "sha512-fAl1W0/7T2G5vURSyxBzrJ1LSdQn6Tr5UX/xD4PXDx/PDgwygedfW6El/KIj3xJ7FU61TTYnc/l/B7P49Eqt6g==", + "version": "16.5.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.5.1.tgz", + "integrity": "sha512-pF73EOsJgwZekbDHEY5VO/yKXUkab/DuvrQB/ANVizbr6UAHJsDdHXuotZYwkJSGQl1JM+ivXaqY+XBDDL4TiA==", "dev": true, "requires": { - "abab": "^2.0.0", - "acorn": "^7.1.0", - "acorn-globals": "^4.3.2", - "array-equal": "^1.0.0", - "cssom": "^0.4.1", - "cssstyle": "^2.0.0", - "data-urls": "^1.1.0", - "domexception": "^1.0.1", - "escodegen": "^1.11.1", - "html-encoding-sniffer": "^1.0.2", + "abab": "^2.0.5", + "acorn": "^8.0.5", + "acorn-globals": "^6.0.0", + "cssom": "^0.4.4", + "cssstyle": "^2.3.0", + "data-urls": "^2.0.0", + "decimal.js": "^10.2.1", + "domexception": "^2.0.1", + "escodegen": "^2.0.0", + "html-encoding-sniffer": "^2.0.1", + "is-potential-custom-element-name": "^1.0.0", "nwsapi": "^2.2.0", - "parse5": "5.1.0", - "pn": "^1.1.0", - "request": "^2.88.0", - "request-promise-native": "^1.0.7", - "saxes": "^3.1.9", - "symbol-tree": "^3.2.2", - "tough-cookie": "^3.0.1", - "w3c-hr-time": "^1.0.1", - "w3c-xmlserializer": "^1.1.2", - "webidl-conversions": "^4.0.2", + "parse5": "6.0.1", + "request": "^2.88.2", + "request-promise-native": "^1.0.9", + "saxes": "^5.0.1", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.0.0", + "w3c-hr-time": "^1.0.2", + "w3c-xmlserializer": "^2.0.0", + "webidl-conversions": "^6.1.0", "whatwg-encoding": "^1.0.5", "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^7.0.0", - "ws": "^7.0.0", + "whatwg-url": "^8.0.0", + "ws": "^7.4.4", "xml-name-validator": "^3.0.0" + }, + "dependencies": { + "acorn": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.1.0.tgz", + "integrity": "sha512-LWCF/Wn0nfHOmJ9rzQApGnxnvgfROzGilS8936rqN/lfcYkY9MYZzdMqN+2NJ4SlTc+m5HiSa+kNfDtI64dwUA==", + "dev": true + }, + "tr46": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.0.2.tgz", + "integrity": "sha512-3n1qG+/5kg+jrbTzwAykB5yRYtQCTqOGKq5U5PE3b0a1/mzo6snDhjGS0zJVJunO0NrT3Dg1MLy5TjWP/UJppg==", + "dev": true, + "requires": { + "punycode": "^2.1.1" + } + }, + "webidl-conversions": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", + "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", + "dev": true + }, + "whatwg-url": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.4.0.tgz", + "integrity": "sha512-vwTUFf6V4zhcPkWp/4CQPr1TW9Ml6SF4lVyaIMBdJw5i6qUUJ1QWM4Z6YYVkfka0OUIzVo/0aNtGVGk256IKWw==", + "dev": true, + "requires": { + "lodash.sortby": "^4.7.0", + "tr46": "^2.0.2", + "webidl-conversions": "^6.1.0" + } + } } }, "jsesc": { @@ -15829,6 +17968,12 @@ "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", "dev": true }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, "json-schema": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", @@ -16589,15 +18734,6 @@ } } }, - "lolex": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/lolex/-/lolex-5.1.2.tgz", - "integrity": "sha512-h4hmjAvHTmd+25JSwrtTIuwbKdwg5NzZVRMLn9saij4SZaepCrTCxPr35H/3bjwfMJtN+t3CX8672UIkglz28A==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1.7.0" - } - }, "loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -16642,6 +18778,12 @@ "semver": "^5.6.0" } }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, "make-fetch-happen": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-5.0.2.tgz", @@ -16963,6 +19105,17 @@ "mkdirp": "^0.5.1", "rimraf": "^2.5.4", "run-queue": "^1.0.3" + }, + "dependencies": { + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + } } }, "ms": { @@ -17080,6 +19233,15 @@ "which": "^1.3.1" }, "dependencies": { + "rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, "which": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", @@ -17104,35 +19266,53 @@ "dev": true }, "node-notifier": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-6.0.0.tgz", - "integrity": "sha512-SVfQ/wMw+DesunOm5cKqr6yDcvUTDl/yc97ybGHMrteNEY6oekXpNpS3lZwgLlwz0FLgHoiW28ZpmBHUDg37cw==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.2.tgz", + "integrity": "sha512-oJP/9NAdd9+x2Q+rfphB2RJCHjod70RcRLjosiPMMu5gjIfwVnOUGq2nbTjTUbmy0DJ/tFIVT30+Qe3nzl4TJg==", "dev": true, "optional": true, "requires": { "growly": "^1.3.0", - "is-wsl": "^2.1.1", - "semver": "^6.3.0", + "is-wsl": "^2.2.0", + "semver": "^7.3.2", "shellwords": "^0.1.1", - "which": "^1.3.1" + "uuid": "^8.3.0", + "which": "^2.0.2" }, "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "optional": true - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dev": true, "optional": true, "requires": { - "isexe": "^2.0.0" + "yallist": "^4.0.0" } + }, + "semver": { + "version": "7.3.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", + "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", + "dev": true, + "optional": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "optional": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "optional": true } } }, @@ -17498,9 +19678,9 @@ } }, "p-each-series": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.1.0.tgz", - "integrity": "sha512-ZuRs1miPT4HrjFa+9fRfOFXxGJfORgelKV9f9nNOWw2gl6gVsRaVDOQP0+MI0G0wGKns1Yacsu0GjOFbTK0JFQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.2.0.tgz", + "integrity": "sha512-ycIL2+1V32th+8scbpTvyHNaHe02z0sjgh91XXjAk+ZeXoPN4Z46DVUnzdso0aX4KckKw0FNNFHdjZ2UsZvxiA==", "dev": true }, "p-finally": { @@ -17636,9 +19816,9 @@ } }, "parse5": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.0.tgz", - "integrity": "sha512-fxNG2sQjHvlVAYmzBZS9YlDp6PTSSDwa98vkD4QgVDDCAo84z5X1t5XyJQ62ImdLXx5NdIIfihey6xpum9/gRQ==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", "dev": true }, "pascalcase": { @@ -17790,12 +19970,6 @@ "semver-compare": "^1.0.0" } }, - "pn": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", - "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==", - "dev": true - }, "posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", @@ -17808,31 +19982,16 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, - "prettier": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz", - "integrity": "sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew==", - "dev": true - }, - "prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", - "dev": true, - "requires": { - "fast-diff": "^1.1.2" - } - }, "pretty-format": { - "version": "25.5.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-25.5.0.tgz", - "integrity": "sha512-kbo/kq2LQ/A/is0PQwsEHM7Ca6//bGPPvU6UnsdDRSKTWxT/ru/xb88v4BJf6a69H+uTytOEsTusT9ksd/1iWQ==", + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", + "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", "dev": true, "requires": { - "@jest/types": "^25.5.0", + "@jest/types": "^26.6.2", "ansi-regex": "^5.0.0", "ansi-styles": "^4.0.0", - "react-is": "^16.12.0" + "react-is": "^17.0.1" }, "dependencies": { "ansi-regex": { @@ -17842,12 +20001,11 @@ "dev": true }, "ansi-styles": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.2.1.tgz", - "integrity": "sha512-9VGjrMsG1vePxcSweQsN20KY/c4zN0h9fLjqAbwbPfahM3t+NL+M9HC8xeXG2I8pX5NoamTGNuomEUFI7fcUjA==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, "requires": { - "@types/color-name": "^1.1.1", "color-convert": "^2.0.1" } }, @@ -17865,6 +20023,12 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true + }, + "react-is": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz", + "integrity": "sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA==", + "dev": true } } }, @@ -17897,13 +20061,13 @@ } }, "prompts": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.3.2.tgz", - "integrity": "sha512-Q06uKs2CkNYVID0VqwfAl9mipo99zkBv/n2JtWY89Yxa3ZabWSrs0e2KTudKVa3peLUvYXMefDqIleLPVUBZMA==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.0.tgz", + "integrity": "sha512-awZAKrk3vN6CroQukBL+R9051a4R3zCZBlJm/HBfrSZ8iTpYix3VX1vU4mveiLpiwmOJT4wokTF9m6HUk4KqWQ==", "dev": true, "requires": { "kleur": "^3.0.3", - "sisteransi": "^1.0.4" + "sisteransi": "^1.0.5" } }, "promzard": { @@ -18004,6 +20168,12 @@ "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", "dev": true }, + "queue-microtask": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.2.tgz", + "integrity": "sha512-dB15eXv3p2jDlbOiNLyMabYg1/sXvppd8DP2J3EOCQ0AkuSXCW2tP7mnVouVLJKgUMY6yP0kcQDVpLCN13h4Xg==", + "dev": true + }, "quick-lru": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", @@ -18169,12 +20339,6 @@ "readable-stream": "^2.0.2" } }, - "realpath-native": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/realpath-native/-/realpath-native-2.0.0.tgz", - "integrity": "sha512-v1SEYUOXXdbBZK8ZuNgO4TBjamPsiSgcFr0aP+tEKpQZK8vooEUqV6nm6Cv502mX4NF2EfsnVqtNAHG+/6Ur1Q==", - "dev": true - }, "redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -18458,10 +20622,16 @@ "integrity": "sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q=", "dev": true }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", "dev": true, "requires": { "glob": "^7.1.3" @@ -18479,6 +20649,15 @@ "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", "dev": true }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, "run-queue": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", @@ -18542,12 +20721,12 @@ "dev": true }, "saxes": { - "version": "3.1.11", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-3.1.11.tgz", - "integrity": "sha512-Ydydq3zC+WYDJK1+gRxRapLIED9PWeSuuS41wqyoRmzvhhh9nc+QQrVMKJYzJFULazeGhzSV0QleN2wD3boh2g==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", "dev": true, "requires": { - "xmlchars": "^2.1.1" + "xmlchars": "^2.2.0" } }, "schema-utils": { @@ -18661,12 +20840,6 @@ "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "dev": true }, - "slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", - "dev": true - }, "slice-ansi": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-2.1.0.tgz", @@ -18972,10 +21145,21 @@ } }, "stack-utils": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.2.tgz", - "integrity": "sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA==", - "dev": true + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-gL//fkxfWUsIlFL2Tl42Cl6+HFALEaB1FU76I/Fy+oZjRreP7OPMXFlGbxM7NQsI0ZpUfw76sHnv0WNYuTb7Iw==", + "dev": true, + "requires": { + "escape-string-regexp": "^2.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true + } + } }, "static-extend": { "version": "0.1.2", @@ -19027,28 +21211,28 @@ "dev": true }, "string-length": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-3.1.0.tgz", - "integrity": "sha512-Ttp5YvkGm5v9Ijagtaz1BnN+k9ObpvS0eIBblPMp2YWL8FBmi9qblQ9fexc2k/CXFgrTIteU3jAw3payCnwSTA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", "dev": true, "requires": { - "astral-regex": "^1.0.0", - "strip-ansi": "^5.2.0" + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" }, "dependencies": { "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", + "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", "dev": true }, "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", + "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", "dev": true, "requires": { - "ansi-regex": "^4.1.0" + "ansi-regex": "^5.0.0" } } } @@ -19214,9 +21398,9 @@ "dev": true }, "supports-color": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.1.0.tgz", - "integrity": "sha512-oRSIpR8pxT1Wr2FquTNnGet79b3BWljqOuoW/h4oBhxJ/HUbX5nX6JSruTkvXDCFMwDPvsaTTbvMLKZWSy0R5g==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "requires": { "has-flag": "^4.0.0" @@ -19464,14 +21648,14 @@ } }, "tough-cookie": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-3.0.1.tgz", - "integrity": "sha512-yQyJ0u4pZsv9D4clxO69OEjLWYw+jbgspjTue4lTQZLfV0c5l1VmK2y1JK8E9ahdpltPOaAThPcp5nKPUgSnsg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", + "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", "dev": true, "requires": { - "ip-regex": "^2.1.0", - "psl": "^1.1.28", - "punycode": "^2.1.1" + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.1.2" } }, "tr46": { @@ -19495,6 +21679,174 @@ "integrity": "sha1-n5up2e+odkw4dpi8v+sshI8RrbM=", "dev": true }, + "ts-jest": { + "version": "26.5.6", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-26.5.6.tgz", + "integrity": "sha512-rua+rCP8DxpA8b4DQD/6X2HQS8Zy/xzViVYfEs2OQu68tkCuKLV0Md8pmX55+W24uRIyAsf/BajRfxOs+R2MKA==", + "dev": true, + "requires": { + "bs-logger": "0.x", + "buffer-from": "1.x", + "fast-json-stable-stringify": "2.x", + "jest-util": "^26.1.0", + "json5": "2.x", + "lodash": "4.x", + "make-error": "1.x", + "mkdirp": "1.x", + "semver": "7.x", + "yargs-parser": "20.x" + }, + "dependencies": { + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "chalk": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", + "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "jest-util": { + "version": "26.6.2", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", + "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + } + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "micromatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", + "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + } + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "picomatch": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.3.tgz", + "integrity": "sha512-KpELjfwcCDUb9PeigTs2mBJzXUPzAuP2oPcA989He8Rte0+YUAjw1JVedDhuTKPkHjSYzMN3npC9luThGYEKdg==", + "dev": true + }, + "semver": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", + "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "yargs-parser": { + "version": "20.2.7", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz", + "integrity": "sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==", + "dev": true + } + } + }, "tsconfig-paths": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz", @@ -19524,6 +21876,15 @@ "integrity": "sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q==", "dev": true }, + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, "tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -19555,9 +21916,9 @@ "dev": true }, "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.0.2.tgz", + "integrity": "sha512-a720oz3Kjbp3ll0zkeN9qjRhO7I34MKMhPGQiQJAmaZQZQ1lo+NWThK322f7sXV+kTg9B1Ybt16KgBXWgteT8w==", "dev": true }, "typedarray": { @@ -19575,6 +21936,12 @@ "is-typedarray": "^1.0.0" } }, + "typescript": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.3.tgz", + "integrity": "sha512-qOcYwxaByStAWrBf4x0fibwZvMRG+r4cQoTjbPtUlrWjBHbmCAww1i448U0GJ+3cNNEtebDteo/cHOR3xJ4wEw==", + "dev": true + }, "uglify-js": { "version": "3.10.0", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.10.0.tgz", @@ -19762,9 +22129,9 @@ "dev": true }, "v8-to-istanbul": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-4.1.4.tgz", - "integrity": "sha512-Rw6vJHj1mbdK8edjR7+zuJrpDtKIgNdAvTSAcpYfgMIw+u2dPDntD3dgN4XQFLU2/fvFQdzj+EeSGfd/jnY5fQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-7.1.0.tgz", + "integrity": "sha512-uXUVqNUCLa0AH1vuVxzi+MI4RfxEOKt9pBgKwHbgH7st8Kv2P1m+jvWNnektzBh5QShF3ODgKmUFCf38LnVz1g==", "dev": true, "requires": { "@types/istanbul-lib-coverage": "^2.0.1", @@ -19857,13 +22224,11 @@ } }, "w3c-xmlserializer": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-1.1.2.tgz", - "integrity": "sha512-p10l/ayESzrBMYWRID6xbuCKh2Fp77+sA0doRuGn4tTIMrrZVeqfpKjXHY+oDh3K4nLdPgNwMTVP6Vp4pvqbNg==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", + "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", "dev": true, "requires": { - "domexception": "^1.0.1", - "webidl-conversions": "^4.0.2", "xml-name-validator": "^3.0.0" } }, @@ -20184,9 +22549,9 @@ } }, "ws": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.3.1.tgz", - "integrity": "sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA==", + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz", + "integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==", "dev": true }, "xml-name-validator": { @@ -20263,9 +22628,9 @@ "dev": true }, "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", + "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", "dev": true, "requires": { "emoji-regex": "^8.0.0", diff --git a/package.json b/package.json index 3c271db413..1d54ccf173 100644 --- a/package.json +++ b/package.json @@ -10,15 +10,15 @@ }, "homepage": "https://github.com/kong/insomnia#readme", "scripts": { - "lint": "eslint . --ext .js,.json", - "lint:fix": "npm run lint -- --fix", + "build": "lerna run build --parallel", + "lint": "lerna run lint --parallel --stream --no-bail", + "lint:fix": "lerna run lint:fix --parallel --stream --no-bail", "bootstrap": "npm install && lerna bootstrap && lerna run --parallel --stream bootstrap", "version": "lerna version --exact --preid beta --force-publish", "version:dry": "npm run version -- --no-git-tag-version", "publish": "lerna publish from-git --pre-dist-tag beta", "clean": "lerna clean --yes && rimraf node_modules", - "typecheck": "lerna run --parallel --stream typecheck", - "test": "npm run lint && npm run typecheck && lerna run --stream --parallel test", + "test": "lerna run --stream --parallel test", "test:pre-release": "npm run test --prefix packages/insomnia-app", "inso-start": "npm start --prefix packages/insomnia-inso", "app-start": "npm start --prefix packages/insomnia-app", @@ -35,7 +35,7 @@ "test:smoke:cli": "npm run test:cli --prefix packages/insomnia-smoke-test" }, "lint-staged": { - "{packages,plugins}/**/*.{js,json}": [ + "{packages,plugins}/**/*.{js,json,ts,tsx}": [ "eslint --fix" ] }, @@ -51,14 +51,19 @@ "@babel/preset-env": "^7.4.3", "@babel/preset-flow": "^7.9.0", "@babel/preset-react": "^7.9.4", - "babel-eslint": "^10.1.0", - "babel-jest": "^25.3.0", + "@jest/types": "^26.6.2", + "@types/chai": "^4.2.15", + "@types/eslint": "4.16.1", + "@types/jest": "^26.0.23", + "@types/node": "^14.14.32", + "@types/rimraf": "^3.0.0", + "@typescript-eslint/eslint-plugin": "^4.16.1", + "@typescript-eslint/parser": "^4.16.1", "babel-loader": "^8.0.5", "babel-plugin-inline-react-svg": "^1.1.0", - "babel-plugin-styled-components": "^1.10.6", + "babel-plugin-styled-components": "^1.12.0", "cross-env": "^7.0.2", "eslint": "^7.2.0", - "eslint-config-prettier": "^6.11.0", "eslint-config-semistandard": "^15.0.0", "eslint-config-standard": "^14.1.1", "eslint-plugin-filenames": "^1.2.0", @@ -66,19 +71,20 @@ "eslint-plugin-html": "^6.0.1", "eslint-plugin-import": "^2.20.2", "eslint-plugin-jest": "^21.15.1", + "eslint-plugin-jest-formatting": "^2.0.1", "eslint-plugin-json": "^1.2.0", "eslint-plugin-node": "^6.0.1", - "eslint-plugin-prettier": "^3.1.3", "eslint-plugin-promise": "^3.7.0", "eslint-plugin-react": "^7.19.0", "eslint-plugin-react-hooks": "^4.0.4", "eslint-plugin-standard": "^4.0.1", - "flow-bin": "^0.122.0", "husky": "^4.2.5", - "jest": "^25.3.0", + "jest": "^26.6.3", "lerna": "^3.22.0", "lint-staged": "^10.2.2", - "prettier": "^1.16.4", - "rimraf": "^2.6.3" + "rimraf": "^3.0.2", + "ts-jest": "^26.5.6", + "type-fest": "^1.0.2", + "typescript": "^4.2.3" } } diff --git a/packages/insomnia-app/.babelrc b/packages/insomnia-app/.babelrc deleted file mode 100644 index 3f3ed6140e..0000000000 --- a/packages/insomnia-app/.babelrc +++ /dev/null @@ -1,38 +0,0 @@ -{ - "presets": [ - "@babel/preset-react", - "@babel/preset-flow" - ], - "plugins": [ - "babel-plugin-styled-components", - [ - "@babel/plugin-proposal-decorators", - { - "legacy": true - } - ], - ["@babel/plugin-proposal-class-properties", { "loose" : true }], - "@babel/plugin-proposal-optional-chaining" - ], - "env": { - "development": { - "plugins": [ - "react-hot-loader/babel" - ] - }, - "test": { - "presets": [ - [ - "@babel/preset-env", - { - "targets": { - "node": "12" - } - } - ], - // We need to add this again because it has to run before es2015 - "@babel/preset-flow" - ] - } - } -} diff --git a/packages/insomnia-app/.eslintignore b/packages/insomnia-app/.eslintignore new file mode 100644 index 0000000000..1a729db3be --- /dev/null +++ b/packages/insomnia-app/.eslintignore @@ -0,0 +1,7 @@ +build +bin +send-request +**/main.min.js +webpack.config.*.js +webpack.config.*.js.map +webpack.config.*.d.ts \ No newline at end of file diff --git a/packages/insomnia-app/.eslintrc.js b/packages/insomnia-app/.eslintrc.js new file mode 100644 index 0000000000..4054179923 --- /dev/null +++ b/packages/insomnia-app/.eslintrc.js @@ -0,0 +1,12 @@ +/** @type { import('eslint').Linter.Config } */ +module.exports = { + extends: '../../.eslintrc.js', + rules: { + 'filenames/match-exported': 'off', + camelcase: 'off', + '@typescript-eslint/array-type': ['error', { default: 'generic', readonly: 'generic' }], + '@typescript-eslint/no-use-before-define': 'off', // TSCONVERSION + '@typescript-eslint/no-explicit-any': 'off', // TSCONVERSION + // 'padding-line-between-statements': ['error', { blankLine: "always", prev: ["*"], next: "export"}], + }, +}; diff --git a/packages/insomnia-app/.flowconfig b/packages/insomnia-app/.flowconfig deleted file mode 100644 index 0ec87474c0..0000000000 --- a/packages/insomnia-app/.flowconfig +++ /dev/null @@ -1,20 +0,0 @@ -[ignore] -.*/node_modules/.* -.*/__fixtures__/.* -!/node_modules/graphql -!/node_modules/iterall - -[include] - -[libs] - -[options] -esproposal.decorators=ignore -module.file_ext=.css -esproposal.optional_chaining=enable - -[lints] - -[untyped] -/node_modules/graphql -/node_modules/iterall diff --git a/packages/insomnia-app/.gitignore b/packages/insomnia-app/.gitignore index 935bdcd88d..96d3f85116 100644 --- a/packages/insomnia-app/.gitignore +++ b/packages/insomnia-app/.gitignore @@ -2,5 +2,4 @@ dist build # Generated -app/main.min.js - +app/main.min.js \ No newline at end of file diff --git a/packages/insomnia-app/.storybook/config.js b/packages/insomnia-app/.storybook/config.js index 247b9abc1f..3cb8f264b7 100644 --- a/packages/insomnia-app/.storybook/config.js +++ b/packages/insomnia-app/.storybook/config.js @@ -1,10 +1,6 @@ import { configure, addDecorator } from '@storybook/react'; import { withInfo } from '@storybook/addon-info'; -addDecorator( - withInfo({ - inline: true, - }), -); +addDecorator(withInfo({ inline: true })); configure(require.context('../app/ui', true, /\.stories\.js$/), module); diff --git a/packages/insomnia-app/.storybook/webpack.config.js b/packages/insomnia-app/.storybook/webpack.config.js index 669d8e4bf2..1a4ab2d7bb 100644 --- a/packages/insomnia-app/.storybook/webpack.config.js +++ b/packages/insomnia-app/.storybook/webpack.config.js @@ -1,4 +1,5 @@ -module.exports = async function({ config, mode }) { +/** @type { import('webpack').Configuration } */ +module.exports = ({ config }) => { config.module.rules.push({ test: /\.less$/, use: [ diff --git a/packages/insomnia-app/app/__jest__/before-each.js b/packages/insomnia-app/app/__jest__/before-each.ts similarity index 59% rename from packages/insomnia-app/app/__jest__/before-each.js rename to packages/insomnia-app/app/__jest__/before-each.ts index 7f2189fea9..a359be4e65 100644 --- a/packages/insomnia-app/app/__jest__/before-each.js +++ b/packages/insomnia-app/app/__jest__/before-each.ts @@ -1,10 +1,16 @@ -import * as db from '../common/database'; +import { database as db } from '../common/database'; import * as models from '../models'; import * as fetch from '../account/fetch'; export async function globalBeforeEach() { // Setup the local database in case it's used - fetch.setup('insomnia-tests', 'http://localhost:8000'); - await db.init(models.types(), { inMemoryOnly: true }, true, () => {}); + await db.init( + models.types(), + { + inMemoryOnly: true, + }, + true, + () => {}, + ); } diff --git a/packages/insomnia-app/app/__jest__/redux-state-for-test.js b/packages/insomnia-app/app/__jest__/redux-state-for-test.ts similarity index 77% rename from packages/insomnia-app/app/__jest__/redux-state-for-test.js rename to packages/insomnia-app/app/__jest__/redux-state-for-test.ts index 6141928d7c..05bc282263 100644 --- a/packages/insomnia-app/app/__jest__/redux-state-for-test.js +++ b/packages/insomnia-app/app/__jest__/redux-state-for-test.ts @@ -1,10 +1,10 @@ -// @flow - import * as entities from '../ui/redux/modules/entities'; -const reduxStateForTest = async (activeWorkspaceId: string): Promise => ({ +const reduxStateForTest = async (activeWorkspaceId: string): Promise> => ({ entities: entities.reducer({}, entities.initializeWith(await entities.allDocs())), - global: { activeWorkspaceId }, + global: { + activeWorkspaceId, + }, }); export default reduxStateForTest; diff --git a/packages/insomnia-app/app/__jest__/setup-after-env.js b/packages/insomnia-app/app/__jest__/setup-after-env.ts similarity index 58% rename from packages/insomnia-app/app/__jest__/setup-after-env.js rename to packages/insomnia-app/app/__jest__/setup-after-env.ts index 666127af39..22c352d6a0 100644 --- a/packages/insomnia-app/app/__jest__/setup-after-env.js +++ b/packages/insomnia-app/app/__jest__/setup-after-env.ts @@ -1 +1,2 @@ +import '@testing-library/jest-dom'; import '@testing-library/jest-dom/extend-expect'; diff --git a/packages/insomnia-app/app/__jest__/setup.js b/packages/insomnia-app/app/__jest__/setup.js deleted file mode 100644 index f6191a5127..0000000000 --- a/packages/insomnia-app/app/__jest__/setup.js +++ /dev/null @@ -1,30 +0,0 @@ -import 'whatwg-fetch'; - -const localStorageMock = (function() { - let store = {}; - - return { - getItem(key) { - return store[key]; - }, - setItem(key, value) { - store[key] = value.toString(); - }, - clear() { - store = {}; - }, - }; -})(); - -global.__DEV__ = false; -global.localStorage = localStorageMock; -global.requestAnimationFrame = cb => process.nextTick(cb); -global.require = require; - -// Don't console log real logs that start with a tag (eg. [db] ...). It's annoying -const log = console.log; -global.console.log = (...args) => { - if (!(typeof args[0] === 'string' && args[0][0] === '[')) { - log(...args); - } -}; diff --git a/packages/insomnia-app/app/__jest__/setup.ts b/packages/insomnia-app/app/__jest__/setup.ts new file mode 100644 index 0000000000..0167347440 --- /dev/null +++ b/packages/insomnia-app/app/__jest__/setup.ts @@ -0,0 +1,49 @@ +import 'whatwg-fetch'; + +const localStorageMock: Storage = (function() { + let store: Record = {}; + return { + get length() { + return Object.keys(store).length; + }, + + clear() { + store = {}; + }, + + getItem(key: string) { + return store[key]; + }, + + key() { + return null; + }, + + removeItem(key: string) { + delete store[key]; + }, + + setItem(key: string, value: string) { + store[key] = value.toString(); + }, + }; +})(); + +global.__DEV__ = false; +global.localStorage = localStorageMock; + +global.requestAnimationFrame = (callback: FrameRequestCallback) => { + process.nextTick(callback); + // note: the spec indicates that the return of this function (the request id) is a non-zero number. hopefully returning 0 here will indicate that this is a mock if the return is ever to be used accidentally. + return 0; +}; + +global.require = require; +// Don't console log real logs that start with a tag (eg. [db] ...). It's annoying +const log = console.log; + +global.console.log = (...args) => { + if (!(typeof args[0] === 'string' && args[0][0] === '[')) { + log(...args); + } +}; diff --git a/packages/insomnia-app/app/__mocks__/@grpc/grpc-js.js b/packages/insomnia-app/app/__mocks__/@grpc/grpc-js.ts similarity index 95% rename from packages/insomnia-app/app/__mocks__/@grpc/grpc-js.js rename to packages/insomnia-app/app/__mocks__/@grpc/grpc-js.ts index 4b85bc7a14..9f69aafd10 100644 --- a/packages/insomnia-app/app/__mocks__/@grpc/grpc-js.js +++ b/packages/insomnia-app/app/__mocks__/@grpc/grpc-js.ts @@ -1,9 +1,12 @@ import { EventEmitter } from 'events'; +const grpcJs = jest.requireActual('@grpc/grpc-js'); const mockCallWrite = jest.fn(); const mockCallEnd = jest.fn(); const mockCallCancel = jest.fn(); +export const status = grpcJs.status; + class MockCall extends EventEmitter { write(...args) { mockCallWrite(...args); diff --git a/packages/insomnia-app/app/__mocks__/dummy.js b/packages/insomnia-app/app/__mocks__/dummy.js deleted file mode 100644 index f053ebf797..0000000000 --- a/packages/insomnia-app/app/__mocks__/dummy.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = {}; diff --git a/packages/insomnia-app/app/__mocks__/dummy.ts b/packages/insomnia-app/app/__mocks__/dummy.ts new file mode 100644 index 0000000000..4d7bf6801f --- /dev/null +++ b/packages/insomnia-app/app/__mocks__/dummy.ts @@ -0,0 +1,2 @@ +// WARNING: changing this to `export default` will break the mock and be incredibly hard to debug. Ask me how I know. +module.exports = {}; diff --git a/packages/insomnia-app/app/__mocks__/electron.js b/packages/insomnia-app/app/__mocks__/electron.ts similarity index 56% rename from packages/insomnia-app/app/__mocks__/electron.js rename to packages/insomnia-app/app/__mocks__/electron.ts index 989667a572..2027125713 100644 --- a/packages/insomnia-app/app/__mocks__/electron.js +++ b/packages/insomnia-app/app/__mocks__/electron.ts @@ -1,11 +1,9 @@ import mkdirp from 'mkdirp'; -import events from 'events'; +import { EventEmitter } from 'events'; import os from 'os'; import path from 'path'; -const RANDOM_STRING = Math.random() - .toString() - .replace('.', ''); +const RANDOM_STRING = Math.random().toString().replace('.', ''); const remote = { app: { @@ -14,14 +12,18 @@ const remote = { mkdirp.sync(dir); return dir; }, + getLocale() { return 'en-US'; }, }, net: { - request(url) { - const req = new events.EventEmitter(); + request() { + const req = new EventEmitter(); + + // @ts-expect-error -- TSCONVERSION appears to be genuine req.end = function() {}; + return req; }, }, @@ -29,32 +31,47 @@ const remote = { getAllWindows() { return []; }, + getFocusedWindow() { return { getContentBounds() { - return { width: 1900, height: 1060 }; + return { + width: 1900, + height: 1060, + }; }, }; }, }, screen: { getPrimaryDisplay() { - return { workAreaSize: { width: 1920, height: 1080 } }; + return { + workAreaSize: { + width: 1920, + height: 1080, + }, + }; }, }, }; -module.exports = { +const electron = { ...remote, - remote: remote, + remote, ipcMain: { on: jest.fn(), + once() {}, }, ipcRenderer: { on: jest.fn(), removeAllListeners: jest.fn(), + once() {}, + send: jest.fn(), }, }; + +// WARNING: changing this to `export default` will break the mock and be incredibly hard to debug. Ask me how I know. +module.exports = electron; diff --git a/packages/insomnia-app/app/__mocks__/font-scanner.js b/packages/insomnia-app/app/__mocks__/font-scanner.js deleted file mode 100644 index f053ebf797..0000000000 --- a/packages/insomnia-app/app/__mocks__/font-scanner.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = {}; diff --git a/packages/insomnia-app/app/__mocks__/font-scanner.ts b/packages/insomnia-app/app/__mocks__/font-scanner.ts new file mode 100644 index 0000000000..4d7bf6801f --- /dev/null +++ b/packages/insomnia-app/app/__mocks__/font-scanner.ts @@ -0,0 +1,2 @@ +// WARNING: changing this to `export default` will break the mock and be incredibly hard to debug. Ask me how I know. +module.exports = {}; diff --git a/packages/insomnia-app/app/__mocks__/isomorphic-git.js b/packages/insomnia-app/app/__mocks__/isomorphic-git.ts similarity index 54% rename from packages/insomnia-app/app/__mocks__/isomorphic-git.js rename to packages/insomnia-app/app/__mocks__/isomorphic-git.ts index cc8aa489fb..62736d2eb7 100644 --- a/packages/insomnia-app/app/__mocks__/isomorphic-git.js +++ b/packages/insomnia-app/app/__mocks__/isomorphic-git.ts @@ -2,6 +2,11 @@ const git = jest.requireActual('isomorphic-git'); const mock = jest.genMockFromModule('isomorphic-git'); +// @ts-expect-error -- TSCONVERSION git.push = mock.push; + +// @ts-expect-error -- TSCONVERSION git.clone = mock.clone; + +// WARNING: changing this to `export default` will break the mock and be incredibly hard to debug. Ask me how I know. module.exports = git; diff --git a/packages/insomnia-app/app/__mocks__/node-forge.js b/packages/insomnia-app/app/__mocks__/node-forge.ts similarity index 79% rename from packages/insomnia-app/app/__mocks__/node-forge.js rename to packages/insomnia-app/app/__mocks__/node-forge.ts index 90850d1607..88b3a08bde 100644 --- a/packages/insomnia-app/app/__mocks__/node-forge.js +++ b/packages/insomnia-app/app/__mocks__/node-forge.ts @@ -3,8 +3,9 @@ * The reason it is needed is because the Forge module loader doesn't * play along with Jest. */ -const forge = require('../../node_modules/node-forge/lib/index'); +import forge from '../../node_modules/node-forge/lib/index'; +// WARNING: changing this to `export default` will break the mock and be incredibly hard to debug. Ask me how I know. module.exports = { jsbn: forge.jsbn, util: forge.util, @@ -17,25 +18,28 @@ module.exports = { rsa: { setPublicKey() { return { - encrypt(str) { + encrypt(str: string) { return str; }, }; }, + setPrivateKey() { return { - decrypt(str) { + decrypt(str: string) { return str; }, }; }, }, random: { - getBytesSync(n) { + getBytesSync(n: number) { let s = ''; + for (let i = 0; i < n; i++) { s += 'a'; } + return s; }, }, @@ -62,28 +66,35 @@ module.exports = { }, }, cipher: { - createCipher(alg, key) { + createCipher() { return { start(config) { this._config = config; }, + update(buffer) { this._data = buffer; }, + finish() { - this.mode = { tag: 'tag' }; + this.mode = { + tag: 'tag', + }; this.output = this._data; }, }; }, - createDecipher(alg, key) { + + createDecipher() { return { start(config) { this._config = config; }, + update(buffer) { this.output = buffer; }, + finish() { return true; }, diff --git a/packages/insomnia-app/app/__mocks__/node-libcurl.js b/packages/insomnia-app/app/__mocks__/node-libcurl.ts similarity index 61% rename from packages/insomnia-app/app/__mocks__/node-libcurl.js rename to packages/insomnia-app/app/__mocks__/node-libcurl.ts index 321ec29569..f04aa16d4c 100644 --- a/packages/insomnia-app/app/__mocks__/node-libcurl.js +++ b/packages/insomnia-app/app/__mocks__/node-libcurl.ts @@ -1,6 +1,5 @@ import { EventEmitter } from 'events'; import fs from 'fs'; - import { CurlAuth } from 'node-libcurl/dist/enum/CurlAuth'; import { CurlCode } from 'node-libcurl/dist/enum/CurlCode'; import { CurlInfoDebug } from 'node-libcurl/dist/enum/CurlInfoDebug'; @@ -9,11 +8,60 @@ import { CurlNetrc } from 'node-libcurl/dist/enum/CurlNetrc'; import { CurlHttpVersion } from 'node-libcurl/dist/enum/CurlHttpVersion'; class Curl extends EventEmitter { - constructor() { - super(); - this._options = {}; - this._meta = {}; - this._features = {}; + _options = {}; + _meta = {}; + _features = {}; + + // cannot include these from node-libcurl because they come from the native library + // and it's not possible to load it while testing (as it was built to run with Electron) + static info = { + COOKIELIST: 'COOKIELIST', + EFFECTIVE_URL: 'EFFECTIVE_URL', + SIZE_DOWNLOAD: 'SIZE_DOWNLOAD', + TOTAL_TIME: 'TOTAL_TIME', + } + + static option = { + ACCEPT_ENCODING: 'ACCEPT_ENCODING', + CAINFO: 'CAINFO', + COOKIEFILE: 'COOKIEFILE', + COOKIELIST: 'COOKIELIST', + CUSTOMREQUEST: 'CUSTOMREQUEST', + DEBUGFUNCTION: 'DEBUGFUNCTION', + FOLLOWLOCATION: 'FOLLOWLOCATION', + HTTPAUTH: 'HTTPAUTH', + HTTPGET: 'HTTPGET', + HTTPHEADER: 'HTTPHEADER', + HTTPPOST: 'HTTPPOST', + HTTP_VERSION: 'HTTP_VERSION', + INFILESIZE_LARGE: 'INFILESIZE_LARGE', + KEYPASSWD: 'KEYPASSWD', + MAXREDIRS: 'MAXREDIRS', + NETRC: 'NETRC', + NOBODY: 'NOBODY', + NOPROGRESS: 'NOPROGRESS', + NOPROXY: 'NOPROXY', + PASSWORD: 'PASSWORD', + POST: 'POST', + POSTFIELDS: 'POSTFIELDS', + PROXY: 'PROXY', + PROXYAUTH: 'PROXYAUTH', + READDATA: 'READDATA', + READFUNCTION: 'READFUNCTION', + SSLCERT: 'SSLCERT', + SSLCERTTYPE: 'SSLCERTTYPE', + SSLKEY: 'SSLKEY', + SSL_VERIFYHOST: 'SSL_VERIFYHOST', + SSL_VERIFYPEER: 'SSL_VERIFYPEER', + TIMEOUT_MS: 'TIMEOUT_MS', + UNIX_SOCKET_PATH: 'UNIX_SOCKET_PATH', + UPLOAD: 'UPLOAD', + URL: 'URL', + USERAGENT: 'USERAGENT', + USERNAME: 'USERNAME', + VERBOSE: 'VERBOSE', + WRITEFUNCTION: 'WRITEFUNCTION', + XFERINFOFUNCTION: 'XFERINFOFUNCTION', } static getVersion() { @@ -36,13 +84,16 @@ class Curl extends EventEmitter { if (name === Curl.option.READFUNCTION) { let body = ''; + // Only limiting this to prevent infinite loops for (let i = 0; i < 1000; i++) { const buffer = Buffer.alloc(23); const bytes = value(buffer); + if (bytes === 0) { break; } + body += buffer.slice(0, bytes); } @@ -52,6 +103,7 @@ class Curl extends EventEmitter { if (name === Curl.option.COOKIELIST) { // This can be set multiple times this._options[name] = this._options[name] || []; + this._options[name].push(value); } else if (name === Curl.option.READDATA) { const { size } = fs.fstatSync(value); @@ -67,12 +119,16 @@ class Curl extends EventEmitter { switch (name) { case Curl.info.COOKIELIST: return [`#HttpOnly_.insomnia.rest\tTRUE\t/url/path\tTRUE\t${Date.now() / 1000}\tfoo\tbar`]; + case Curl.info.EFFECTIVE_URL: return this._options[Curl.option.URL]; + case Curl.info.TOTAL_TIME: return 700; + case Curl.info.SIZE_DOWNLOAD: return 800; + default: throw new Error(`Invalid info ${name}`); } @@ -87,8 +143,9 @@ class Curl extends EventEmitter { features: this._features, }), ); - this.emit('data', data); + + // @ts-expect-error -- TSCONVERSION this._options.WRITEFUNCTION(data); process.nextTick(() => { @@ -110,78 +167,32 @@ class Curl extends EventEmitter { close() {} } -// cannot include these from node-libcurl because they come from the native library -// and it's not possible to load it while testing (as it was built to run with Electron) -Curl.info = { - COOKIELIST: 'COOKIELIST', - EFFECTIVE_URL: 'EFFECTIVE_URL', - SIZE_DOWNLOAD: 'SIZE_DOWNLOAD', - TOTAL_TIME: 'TOTAL_TIME', -}; - -Curl.option = { - ACCEPT_ENCODING: 'ACCEPT_ENCODING', - CAINFO: 'CAINFO', - COOKIEFILE: 'COOKIEFILE', - COOKIELIST: 'COOKIELIST', - CUSTOMREQUEST: 'CUSTOMREQUEST', - DEBUGFUNCTION: 'DEBUGFUNCTION', - FOLLOWLOCATION: 'FOLLOWLOCATION', - HTTPAUTH: 'HTTPAUTH', - HTTPGET: 'HTTPGET', - HTTPHEADER: 'HTTPHEADER', - HTTPPOST: 'HTTPPOST', - HTTP_VERSION: 'HTTP_VERSION', - INFILESIZE_LARGE: 'INFILESIZE_LARGE', - KEYPASSWD: 'KEYPASSWD', - MAXREDIRS: 'MAXREDIRS', - NETRC: 'NETRC', - NOBODY: 'NOBODY', - NOPROGRESS: 'NOPROGRESS', - NOPROXY: 'NOPROXY', - PASSWORD: 'PASSWORD', - POST: 'POST', - POSTFIELDS: 'POSTFIELDS', - PROXY: 'PROXY', - PROXYAUTH: 'PROXYAUTH', - READDATA: 'READDATA', - READFUNCTION: 'READFUNCTION', - SSLCERT: 'SSLCERT', - SSLCERTTYPE: 'SSLCERTTYPE', - SSLKEY: 'SSLKEY', - SSL_VERIFYHOST: 'SSL_VERIFYHOST', - SSL_VERIFYPEER: 'SSL_VERIFYPEER', - TIMEOUT_MS: 'TIMEOUT_MS', - UNIX_SOCKET_PATH: 'UNIX_SOCKET_PATH', - UPLOAD: 'UPLOAD', - URL: 'URL', - USERAGENT: 'USERAGENT', - USERNAME: 'USERNAME', - VERBOSE: 'VERBOSE', - WRITEFUNCTION: 'WRITEFUNCTION', - XFERINFOFUNCTION: 'XFERINFOFUNCTION', -}; - -// This is just to make it easier to test -// node-libcurl Enum exports (CurlAuth, CurlCode, etc) are TypeScript enums, which are -// converted to an object with format: -// { EnumKey: 0, 0: EnumKey } -// We only want the named members (non-number ones) +/** + * This is just to make it easier to test + * node-libcurl Enum exports (CurlAuth, CurlCode, etc) are TypeScript enums, which are converted to an object with format: + * ```ts + * const myEnum = { + * EnumKey: 0, + * 0: EnumKey, + * } + * ``` + * We only want the named members (non-number ones) + */ const getTsEnumOnlyWithNamedMembers = enumObj => { let obj = {}; + for (const member in enumObj) { if (typeof enumObj[member] === 'number') { - obj = { - ...obj, - [member]: member, - }; + obj = { ...obj, [member]: member }; } } + return obj; }; +// WARNING: changing this to `export default` will break the mock and be incredibly hard to debug. Ask me how I know. module.exports = { - Curl: Curl, + Curl, CurlAuth: getTsEnumOnlyWithNamedMembers(CurlAuth), CurlCode: getTsEnumOnlyWithNamedMembers(CurlCode), CurlInfoDebug: getTsEnumOnlyWithNamedMembers(CurlInfoDebug), diff --git a/packages/insomnia-app/app/__tests__/install.test.js b/packages/insomnia-app/app/__tests__/install.test.ts similarity index 94% rename from packages/insomnia-app/app/__tests__/install.test.js rename to packages/insomnia-app/app/__tests__/install.test.ts index 6da6b00164..b88c34ac50 100644 --- a/packages/insomnia-app/app/__tests__/install.test.js +++ b/packages/insomnia-app/app/__tests__/install.test.ts @@ -4,36 +4,29 @@ describe('install.js', () => { describe('containsOnlyDeprecationWarning', () => { it('should return true when all lines in stderr are deprecation warnings', () => { const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); - - const stderr = - // Warning #1 + const stderr = // Warning #1 'warning insomnia-plugin-xxx-yyy > xyz > xyz > xyz > xyz > xyz: ' + 'xyz is no longer maintained and not recommended for usage due to the number of issues. ' + - 'Please, upgrade your dependencies to the actual version of xyz.\r\n' + - // Warning #2 + 'Please, upgrade your dependencies to the actual version of xyz.\r\n' + // Warning #2 'warning insomnia-plugin-xxx-yyy > xyz > xyz > xyz > xyz > xyz: ' + 'xyz is no longer maintained and not recommended for usage due to the number of issues. ' + - 'Please, upgrade your dependencies to the actual version of xyz.\n' + - // Warning #3 + 'Please, upgrade your dependencies to the actual version of xyz.\n' + // Warning #3 'warning insomnia-plugin-xxx-yyy > xyz > xyz > xyz > xyz > xyz: ' + 'xyz is no longer maintained and not recommended for usage due to the number of issues. ' + 'Please, upgrade your dependencies to the actual version of xyz.'; expect(containsOnlyDeprecationWarnings(stderr)).toBe(true); expect(consoleWarnSpy).toHaveBeenCalledTimes(3); }); + it('should return false when stderr contains a deprecation warning and an error', () => { const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); - - const stderr = - // Warning #1 + const stderr = // Warning #1 'warning insomnia-plugin-xxx-yyy > xyz > xyz > xyz > xyz > xyz: ' + 'xyz is no longer maintained and not recommended for usage due to the number of issues. ' + - 'Please, upgrade your dependencies to the actual version of xyz.\r\n' + - // Error #1 + 'Please, upgrade your dependencies to the actual version of xyz.\r\n' + // Error #1 'error https://npm.example.net/@types%example/-/nello-1.3.5.tgz:' + 'Integrity check failed for "@types/example"' + - '(computed integrity doesn\'t match our records, got "sha512-z4kkSfaPg==")\n' + - // Warning #2 + '(computed integrity doesn\'t match our records, got "sha512-z4kkSfaPg==")\n' + // Warning #2 'warning insomnia-plugin-xxx-yyy > xyz > xyz > xyz > xyz > xyz: ' + 'xyz is no longer maintained and not recommended for usage due to the number of issues. ' + 'Please, upgrade your dependencies to the actual version of xyz.'; @@ -41,12 +34,14 @@ describe('install.js', () => { expect(consoleWarnSpy).toHaveBeenCalledTimes(2); }); }); + describe('isDeprecatedDependencies', () => { it('should not match when the message is falsy', () => { expect(isDeprecatedDependencies('')).toBe(false); expect(isDeprecatedDependencies(null)).toBe(false); expect(isDeprecatedDependencies(undefined)).toBe(false); }); + it('should match with multiple nested dependencies', () => { const msg = 'warning insomnia-plugin-xxx-yyy > xyz > xyz > xyz > xyz > xyz: ' + @@ -54,6 +49,7 @@ describe('install.js', () => { 'Please, upgrade your dependencies to the actual version of xyz.'; expect(isDeprecatedDependencies(msg)).toBe(true); }); + it('should match with one nested dependency', () => { const msg = 'warning insomnia-plugin-xxx-yyy > xyz: ' + @@ -61,12 +57,14 @@ describe('install.js', () => { 'Please, upgrade your dependencies to the actual version of xyz.'; expect(isDeprecatedDependencies(msg)).toBe(true); }); + it('should not match with an unrelated warning', () => { const msg = 'warning You are using Node "6.0.0" which is not supported and may encounter bugs or unexpected behaviour. ' + 'Yarn supports the following server range: "^4.8.0 || ^5.7.0 || ^6.2.2 || >=8.0.0"'; expect(isDeprecatedDependencies(msg)).toBe(false); }); + it('should not match with an unrelated error', () => { const msg = 'error https://npm.example.net/@types%example/-/nello-1.3.5.tgz: ' + diff --git a/packages/insomnia-app/app/__tests__/package.test.js b/packages/insomnia-app/app/__tests__/package.test.ts similarity index 99% rename from packages/insomnia-app/app/__tests__/package.test.js rename to packages/insomnia-app/app/__tests__/package.test.ts index bd56a3ca28..4b8b7a098a 100644 --- a/packages/insomnia-app/app/__tests__/package.test.js +++ b/packages/insomnia-app/app/__tests__/package.test.ts @@ -3,6 +3,7 @@ import { globalBeforeEach } from '../__jest__/before-each'; describe('package.json', () => { beforeEach(globalBeforeEach); + it('all packed dependencies should exist', () => { for (const name of globalPackage.packedDependencies) { const version = globalPackage.dependencies[name]; diff --git a/packages/insomnia-app/app/__tests__/renderer.test.js b/packages/insomnia-app/app/__tests__/renderer.test.ts similarity index 99% rename from packages/insomnia-app/app/__tests__/renderer.test.js rename to packages/insomnia-app/app/__tests__/renderer.test.ts index 6a034925e5..dfdc551b73 100644 --- a/packages/insomnia-app/app/__tests__/renderer.test.js +++ b/packages/insomnia-app/app/__tests__/renderer.test.ts @@ -1,7 +1,6 @@ // Broken for now... // import * as renderer from '../renderer'; // import { globalBeforeEach } from '../__jest__/before-each'; - // describe('imports', () => { // beforeEach(globalBeforeEach); // it('ui module should import successfully', () => { diff --git a/packages/insomnia-app/app/account/__tests__/crypt.test.js b/packages/insomnia-app/app/account/__tests__/crypt.test.ts similarity index 99% rename from packages/insomnia-app/app/account/__tests__/crypt.test.js rename to packages/insomnia-app/app/account/__tests__/crypt.test.ts index f87f6921f5..6ed8491a07 100644 --- a/packages/insomnia-app/app/account/__tests__/crypt.test.js +++ b/packages/insomnia-app/app/account/__tests__/crypt.test.ts @@ -5,7 +5,6 @@ describe('crypt', () => { it('derives a key properly', async () => { const result = await crypt.deriveKey('Password', 'email', 'salt'); const expected = 'fb058595c02ae9660ed7098273bf50e49407942ecc437bf317638d76c4578eae'; - expect(result).toBe(expected); }); }); @@ -19,10 +18,8 @@ describe('crypt', () => { key_ops: ['encrypt', 'decrypt'], k: '5hs1f2xuiNPHUp11i6SWlsqYpWe_hWPcEKucZlwBfFE', }; - const resultEncrypted = crypt.encryptAES(key, 'Hello World!', 'additional data'); const resultDecrypted = crypt.decryptAES(key, resultEncrypted); - expect(resultDecrypted).toEqual('Hello World!'); }); }); @@ -36,10 +33,8 @@ describe('crypt', () => { key_ops: ['encrypt', 'decrypt'], k: '5hs1f2xuiNPHUp11i6SWlsqYpWe_hWPcEKucZlwBfFE', }; - const resultEncrypted = crypt.encryptAESBuffer(key, Buffer.from('Hello World!', 'utf8')); const resultDecrypted = crypt.decryptAESToBuffer(key, resultEncrypted); - expect(resultDecrypted).toEqual(Buffer.from('Hello World!', 'utf8')); }); }); @@ -78,7 +73,6 @@ describe('crypt', () => { 'FMVx12_ioueu052xgFxWdIS_lImUGTrw8Iiw_kp-KKsONtofs91A5GVRtyg_wdXpG2qyomaet1hTlHhLnoI23L2a' + 'EkQ87SokIpoR9lR8jfIRwLwKKXMc33_bRRQXvWop0yvTzmSGaC0gULcqj0OHiUR1u9Ver1ZvgGz2jh4mP_E', }; - const publicKey = { alg: 'RSA-OAEP-256', kty: 'RSA', @@ -90,12 +84,9 @@ describe('crypt', () => { 'Om4kAhLVfmdzYB1nZmq9xH1O8_acIUoWDbAWX6fIXhPhn7jCuCO4WZQVDxZ5_bc27UVhR4VYe2Our7aESUQ5Zy' + 'MtYNymo9Oy0y_m3OS6W_JR_feXBbxRCBuGf7fjnvV9ohx1ZqLpJFx9_xL7naoVCQhBDfVE31iYz3L6KTIhFQ', }; - const resultEncrypted = crypt.encryptRSAWithJWK(publicKey, 'aaaaaaaaa'); const resultDecrypted = crypt.decryptRSAWithJWK(privateKey, resultEncrypted); - const expectedDecrypted = 'aaaaaaaaa'; - expect(resultDecrypted.toString()).toEqual(expectedDecrypted); }); }); diff --git a/packages/insomnia-app/app/account/crypt.js b/packages/insomnia-app/app/account/crypt.ts similarity index 86% rename from packages/insomnia-app/app/account/crypt.js rename to packages/insomnia-app/app/account/crypt.ts index d86b0121b0..a84174b154 100644 --- a/packages/insomnia-app/app/account/crypt.js +++ b/packages/insomnia-app/app/account/crypt.ts @@ -36,9 +36,11 @@ export function encryptRSAWithJWK(publicKeyJWK, plaintext) { const encodedPlaintext = encodeURIComponent(plaintext); const n = _b64UrlToBigInt(publicKeyJWK.n); - const e = _b64UrlToBigInt(publicKeyJWK.e); - const publicKey = forge.rsa.setPublicKey(n, e); + const e = _b64UrlToBigInt(publicKeyJWK.e); + + // @ts-expect-error -- TSCONVERSION appears not to be exported for some reason + const publicKey = forge.rsa.setPublicKey(n, e); const encrypted = publicKey.encrypt(encodedPlaintext, 'RSA-OAEP', { md: forge.md.sha256.create(), }); @@ -47,20 +49,27 @@ export function encryptRSAWithJWK(publicKeyJWK, plaintext) { export function decryptRSAWithJWK(privateJWK, encryptedBlob) { const n = _b64UrlToBigInt(privateJWK.n); + const e = _b64UrlToBigInt(privateJWK.e); + const d = _b64UrlToBigInt(privateJWK.d); + const p = _b64UrlToBigInt(privateJWK.p); + const q = _b64UrlToBigInt(privateJWK.q); + const dP = _b64UrlToBigInt(privateJWK.dp); + const dQ = _b64UrlToBigInt(privateJWK.dq); + const qInv = _b64UrlToBigInt(privateJWK.qi); + // @ts-expect-error -- TSCONVERSION appears not to be exported for some reason const privateKey = forge.rsa.setPrivateKey(n, e, d, p, q, dP, dQ, qInv); const bytes = forge.util.hexToBytes(encryptedBlob); const decrypted = privateKey.decrypt(bytes, 'RSA-OAEP', { md: forge.md.sha256.create(), }); - return decodeURIComponent(decrypted); } @@ -76,18 +85,21 @@ export function encryptAESBuffer(jwkOrKey, buff, additionalData = '') { // TODO: Add assertion checks for JWK const rawKey = typeof jwkOrKey === 'string' ? jwkOrKey : _b64UrlToHex(jwkOrKey.k); const key = forge.util.hexToBytes(rawKey); - const iv = forge.random.getBytesSync(12); const cipher = forge.cipher.createCipher('AES-GCM', key); - - cipher.start({ additionalData, iv, tagLength: 128 }); + cipher.start({ + additionalData, + iv, + tagLength: 128, + }); cipher.update(forge.util.createBuffer(buff)); cipher.finish(); - return { iv: forge.util.bytesToHex(iv), + // @ts-expect-error -- TSCONVERSION needs to be converted to string t: forge.util.bytesToHex(cipher.mode.tag), ad: forge.util.bytesToHex(additionalData), + // @ts-expect-error -- TSCONVERSION needs to be converted to string d: forge.util.bytesToHex(cipher.output), }; } @@ -104,21 +116,23 @@ export function encryptAES(jwkOrKey, plaintext, additionalData = '') { // TODO: Add assertion checks for JWK const rawKey = typeof jwkOrKey === 'string' ? jwkOrKey : _b64UrlToHex(jwkOrKey.k); const key = forge.util.hexToBytes(rawKey); - const iv = forge.random.getBytesSync(12); const cipher = forge.cipher.createCipher('AES-GCM', key); - // Plaintext could contain weird unicode, so we have to encode that const encodedPlaintext = encodeURIComponent(plaintext); - - cipher.start({ additionalData, iv, tagLength: 128 }); + cipher.start({ + additionalData, + iv, + tagLength: 128, + }); cipher.update(forge.util.createBuffer(encodedPlaintext)); cipher.finish(); - return { iv: forge.util.bytesToHex(iv), + // @ts-expect-error -- TSCONVERSION needs to be converted to string t: forge.util.bytesToHex(cipher.mode.tag), ad: forge.util.bytesToHex(additionalData), + // @ts-expect-error -- TSCONVERSION needs to be converted to string d: forge.util.bytesToHex(cipher.output), }; } @@ -134,19 +148,17 @@ export function decryptAES(jwkOrKey, encryptedResult) { // TODO: Add assertion checks for JWK const rawKey = typeof jwkOrKey === 'string' ? jwkOrKey : _b64UrlToHex(jwkOrKey.k); const key = forge.util.hexToBytes(rawKey); - // ~~~~~~~~~~~~~~~~~~~~ // // Decrypt with AES-GCM // // ~~~~~~~~~~~~~~~~~~~~ // - const decipher = forge.cipher.createDecipher('AES-GCM', key); decipher.start({ iv: forge.util.hexToBytes(encryptedResult.iv), tagLength: encryptedResult.t.length * 4, + // @ts-expect-error -- TSCONVERSION needs to be converted to string tag: forge.util.hexToBytes(encryptedResult.t), additionalData: forge.util.hexToBytes(encryptedResult.ad), }); - decipher.update(forge.util.createBuffer(forge.util.hexToBytes(encryptedResult.d))); if (decipher.finish()) { @@ -166,22 +178,21 @@ export function decryptAESToBuffer(jwkOrKey, encryptedResult) { // TODO: Add assertion checks for JWK const rawKey = typeof jwkOrKey === 'string' ? jwkOrKey : _b64UrlToHex(jwkOrKey.k); const key = forge.util.hexToBytes(rawKey); - // ~~~~~~~~~~~~~~~~~~~~ // // Decrypt with AES-GCM // // ~~~~~~~~~~~~~~~~~~~~ // - const decipher = forge.cipher.createDecipher('AES-GCM', key); decipher.start({ iv: forge.util.hexToBytes(encryptedResult.iv), tagLength: encryptedResult.t.length * 4, + // @ts-expect-error -- TSCONVERSION needs to be converted to string tag: forge.util.hexToBytes(encryptedResult.t), additionalData: forge.util.hexToBytes(encryptedResult.ad), }); - decipher.update(forge.util.createBuffer(forge.util.hexToBytes(encryptedResult.d))); if (decipher.finish()) { + // @ts-expect-error -- TSCONVERSION needs to be converted to string return Buffer.from(forge.util.bytesToHex(decipher.output), 'hex'); } else { throw new Error('Failed to decrypt data'); @@ -210,14 +221,19 @@ export function srpGenKey() { */ export async function generateAES256Key() { const c = window.crypto; + // @ts-expect-error -- TSCONVERSION: likely needs a module augmentation for webkit const subtle = c ? c.subtle || c.webkitSubtle : null; if (subtle) { console.log('[crypt] Using Native AES Key Generation'); - const key = await subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, [ - 'encrypt', - 'decrypt', - ]); + const key = await subtle.generateKey( + { + name: 'AES-GCM', + length: 256, + }, + true, + ['encrypt', 'decrypt'], + ); return subtle.exportKey('jwk', key); } else { console.log('[crypt] Using Fallback Forge AES Key Generation'); @@ -243,7 +259,6 @@ export async function generateKeyPairJWK() { if (subtle) { console.log('[crypt] Using Native RSA Generation'); - const pair = await subtle.generateKey( { name: 'RSA-OAEP', @@ -254,15 +269,16 @@ export async function generateKeyPairJWK() { true, ['encrypt', 'decrypt'], ); - return { publicKey: await subtle.exportKey('jwk', pair.publicKey), privateKey: await subtle.exportKey('jwk', pair.privateKey), }; } else { console.log('[crypt] Using Forge RSA Generation'); - - const pair = forge.pki.rsa.generateKeyPair({ bits: 2048, e: 0x10001 }); + const pair = forge.pki.rsa.generateKeyPair({ + bits: 2048, + e: 0x10001, + }); const privateKey = { alg: 'RSA-OAEP-256', kty: 'RSA', @@ -277,7 +293,6 @@ export async function generateKeyPairJWK() { q: _bigIntToB64Url(pair.privateKey.q), qi: _bigIntToB64Url(pair.privateKey.qInv), }; - const publicKey = { alg: 'RSA-OAEP-256', kty: 'RSA', @@ -285,8 +300,10 @@ export async function generateKeyPairJWK() { e: _bigIntToB64Url(pair.publicKey.e), n: _bigIntToB64Url(pair.publicKey.n), }; - - return { privateKey, publicKey }; + return { + privateKey, + publicKey, + }; } } @@ -302,7 +319,7 @@ export async function generateKeyPairJWK() { * @returns {Promise} */ async function _hkdfSalt(rawSalt, rawEmail) { - return new Promise(resolve => { + return new Promise(resolve => { const hkdf = new HKDF('sha256', rawSalt, rawEmail); hkdf.derive('', DEFAULT_BYTE_LENGTH, buffer => resolve(buffer.toString('hex'))); }); @@ -321,18 +338,15 @@ function _bigIntToB64Url(n) { function _hexToB64Url(h) { const bytes = forge.util.hexToBytes(h); - return window - .btoa(bytes) - .replace(/=/g, '') - .replace(/\+/g, '-') - .replace(/\//g, '_'); + return window.btoa(bytes).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_'); } -function _b64UrlToBigInt(s) { +function _b64UrlToBigInt(s: string) { + // @ts-expect-error -- TSCONVERSION needs investigation in forge types return new forge.jsbn.BigInteger(_b64UrlToHex(s), 16); } -function _b64UrlToHex(s) { +function _b64UrlToHex(s: string) { const b64 = s.replace(/-/g, '+').replace(/_/g, '/'); return forge.util.bytesToHex(window.atob(b64)); } @@ -346,22 +360,21 @@ function _b64UrlToHex(s) { async function _pbkdf2Passphrase(passphrase, salt) { if (window.crypto && window.crypto.subtle) { console.log('[crypt] Using native PBKDF2'); - const k = await window.crypto.subtle.importKey( 'raw', Buffer.from(passphrase, 'utf8'), - { name: 'PBKDF2' }, + { + name: 'PBKDF2', + }, false, ['deriveBits'], ); - const algo = { name: 'PBKDF2', salt: Buffer.from(salt, 'hex'), iterations: DEFAULT_PBKDF2_ITERATIONS, hash: 'SHA-256', }; - const derivedKeyRaw = await window.crypto.subtle.deriveBits(algo, k, DEFAULT_BYTE_LENGTH * 8); return Buffer.from(derivedKeyRaw).toString('hex'); } else { @@ -373,7 +386,6 @@ async function _pbkdf2Passphrase(passphrase, salt) { DEFAULT_BYTE_LENGTH, forge.md.sha256.create(), ); - return forge.util.bytesToHex(derivedKeyRaw); } } diff --git a/packages/insomnia-app/app/account/fetch.js b/packages/insomnia-app/app/account/fetch.ts similarity index 91% rename from packages/insomnia-app/app/account/fetch.js rename to packages/insomnia-app/app/account/fetch.ts index 267f933e16..2f0a6ee302 100644 --- a/packages/insomnia-app/app/account/fetch.js +++ b/packages/insomnia-app/app/account/fetch.ts @@ -1,17 +1,16 @@ import { delay } from '../common/misc'; import { parse as urlParse } from 'url'; import zlib from 'zlib'; - let _userAgent = ''; let _baseUrl = ''; -const _commandListeners = []; +const _commandListeners: Array = []; export function setup(userAgent, baseUrl) { _userAgent = userAgent; _baseUrl = baseUrl; } -export function onCommand(callback) { +export function onCommand(callback: Function) { _commandListeners.push(callback); } @@ -32,7 +31,11 @@ async function _fetch(method, path, obj, sessionId, compressBody = false, retrie throw new Error(`No session ID provided to ${method}:${path}`); } - const config = { + const config: { + method: string; + headers: HeadersInit; + body?: string | Buffer; + } = { method: method, headers: {}, }; @@ -56,7 +59,9 @@ async function _fetch(method, path, obj, sessionId, compressBody = false, retrie } let response; + const url = _getUrl(path); + try { response = await window.fetch(url, config); @@ -69,12 +74,14 @@ async function _fetch(method, path, obj, sessionId, compressBody = false, retrie } catch (err) { throw new Error(`Failed to fetch '${url}'`); } + const uri = response.headers.get('x-insomnia-command'); uri && _notifyCommandListeners(uri); if (!response.ok) { const err = new Error(`Response ${response.status} for ${path}`); err.message = await response.text(); + // @ts-expect-error -- TSCONVERSION err.statusCode = response.status; throw err; } @@ -96,7 +103,6 @@ function _getUrl(path) { function _notifyCommandListeners(uri) { const parsed = urlParse(uri, true); - const command = `${parsed.hostname}${parsed.pathname}`; const args = JSON.parse(JSON.stringify(parsed.query)); diff --git a/packages/insomnia-app/app/account/session.js b/packages/insomnia-app/app/account/session.ts similarity index 95% rename from packages/insomnia-app/app/account/session.js rename to packages/insomnia-app/app/account/session.ts index 198b3da690..7d9ab73f92 100644 --- a/packages/insomnia-app/app/account/session.js +++ b/packages/insomnia-app/app/account/session.ts @@ -2,11 +2,12 @@ import * as srp from 'srp-js'; import * as crypt from './crypt'; import * as fetch from './fetch'; -const loginCallbacks = []; +const loginCallbacks: Array = []; function _callCallbacks() { const loggedIn = isLoggedIn(); console.log('[session] Sync state changed loggedIn=' + loggedIn); + for (const cb of loginCallbacks) { if (typeof cb === 'function') { cb(loggedIn); @@ -14,7 +15,7 @@ function _callCallbacks() { } } -export function onLoginLogout(callback) { +export function onLoginLogout(callback: Function) { loginCallbacks.push(callback); } @@ -23,14 +24,13 @@ export async function login(rawEmail, rawPassphrase) { // ~~~~~~~~~~~~~~~ // // Sanitize Inputs // // ~~~~~~~~~~~~~~~ // - const email = _sanitizeEmail(rawEmail); + const passphrase = _sanitizePassphrase(rawPassphrase); // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // Fetch Salt and Submit A To Server // // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // - const { saltKey, saltAuth } = await _getAuthSalts(email); const authSecret = await crypt.deriveKey(passphrase, email, saltKey); const secret1 = await crypt.srpGenKey(); @@ -39,6 +39,7 @@ export async function login(rawEmail, rawPassphrase) { Buffer.from(saltAuth, 'hex'), Buffer.from(email, 'utf8'), Buffer.from(authSecret, 'hex'), + // @ts-expect-error -- TSCONVERSION missing type from srpGenKey Buffer.from(secret1, 'hex'), ); const srpA = c.computeA().toString('hex'); @@ -50,11 +51,9 @@ export async function login(rawEmail, rawPassphrase) { }, null, ); - // ~~~~~~~~~~~~~~~~~~~~~ // // Compute and Submit M1 // // ~~~~~~~~~~~~~~~~~~~~~ // - c.setB(Buffer.from(srpB, 'hex')); const srpM1 = c.computeM1().toString('hex'); const { srpM2 } = await fetch.post( @@ -65,20 +64,15 @@ export async function login(rawEmail, rawPassphrase) { }, null, ); - // ~~~~~~~~~~~~~~~~~~~~~~~~~ // // Verify Server Identity M2 // // ~~~~~~~~~~~~~~~~~~~~~~~~~ // - c.checkM2(Buffer.from(srpM2, 'hex')); - // ~~~~~~~~~~~~~~~~~~~~~~ // // Initialize the Session // // ~~~~~~~~~~~~~~~~~~~~~~ // - // Compute K (used for session ID) const sessionId = c.computeK().toString('hex'); - // Get and store some extra info (salts and keys) const { publicKey, @@ -89,10 +83,8 @@ export async function login(rawEmail, rawPassphrase) { firstName, lastName, } = await _whoami(sessionId); - const derivedSymmetricKey = await crypt.deriveKey(passphrase, email, saltEnc); const symmetricKeyStr = await crypt.decryptAES(derivedSymmetricKey, JSON.parse(encSymmetricKey)); - // Store the information for later setSessionData( sessionId, @@ -107,20 +99,18 @@ export async function login(rawEmail, rawPassphrase) { _callCallbacks(); } - export async function changePasswordWithToken(rawNewPassphrase, confirmationCode) { // Sanitize inputs const newPassphrase = _sanitizePassphrase(rawNewPassphrase); + const newEmail = getEmail(); // Use the same one // Fetch some things const { saltEnc, encSymmetricKey } = await _whoami(); const { saltKey, saltAuth } = await _getAuthSalts(newEmail); - // Generate some secrets for the user base'd on password const newSecret = await crypt.deriveKey(newPassphrase, newEmail, saltEnc); const newAuthSecret = await crypt.deriveKey(newPassphrase, newEmail, saltKey); - const newVerifier = srp .computeVerifier( _getSrpParams(), @@ -129,12 +119,10 @@ export async function changePasswordWithToken(rawNewPassphrase, confirmationCode Buffer.from(newAuthSecret, 'hex'), ) .toString('hex'); - // Re-encrypt existing keys with new secret const symmetricKey = JSON.stringify(_getSymmetricKey()); const newEncSymmetricKeyJSON = crypt.encryptAES(newSecret, symmetricKey); const newEncSymmetricKey = JSON.stringify(newEncSymmetricKeyJSON); - return fetch.post( '/auth/change-password', { @@ -147,21 +135,18 @@ export async function changePasswordWithToken(rawNewPassphrase, confirmationCode getCurrentSessionId(), ); } - export function sendPasswordChangeCode() { return fetch.post('/auth/send-password-code', null, getCurrentSessionId()); } - export function getPublicKey() { return _getSessionData().publicKey; } - export function getPrivateKey() { const { symmetricKey, encPrivateKey } = _getSessionData(); + const privateKeyStr = crypt.decryptAES(symmetricKey, encPrivateKey); return JSON.parse(privateKeyStr); } - export function getCurrentSessionId() { if (window) { return window.localStorage.getItem('currentSessionId'); @@ -169,23 +154,18 @@ export function getCurrentSessionId() { return ''; } } - export function getAccountId() { return _getSessionData().accountId; } - export function getEmail() { return _getSessionData().email; } - export function getFirstName() { return _getSessionData().firstName; } - export function getLastName() { return _getSessionData().lastName; } - export function getFullName() { return `${getFirstName()} ${getLastName()}`.trim(); } @@ -206,6 +186,7 @@ export async function logout() { } _unsetSessionData(); + _callCallbacks(); } @@ -230,17 +211,13 @@ export function setSessionData( firstName: firstName, lastName: lastName, }); - window.localStorage.setItem(_getSessionKey(sessionId), dataStr); - // NOTE: We're setting this last because the stuff above might fail window.localStorage.setItem('currentSessionId', sessionId); } - export async function listTeams() { return fetch.get('/api/teams', getCurrentSessionId()); } - export async function endTrial() { await fetch.put('/api/billing/end-trial', null, getCurrentSessionId()); } @@ -248,9 +225,9 @@ export async function endTrial() { // ~~~~~~~~~~~~~~~~ // // Helper Functions // // ~~~~~~~~~~~~~~~~ // - function _getSymmetricKey() { const sessionData = _getSessionData(); + return sessionData.symmetricKey; } @@ -259,16 +236,26 @@ function _whoami(sessionId = null) { } function _getAuthSalts(email) { - return fetch.post('/auth/login-s', { email }, getCurrentSessionId()); + return fetch.post( + '/auth/login-s', + { + email, + }, + getCurrentSessionId(), + ); } function _getSessionData() { const sessionId = getCurrentSessionId(); + if (!sessionId || !window) { return {}; } const dataStr = window.localStorage.getItem(_getSessionKey(sessionId)); + if (dataStr === null) { + return null; + } return JSON.parse(dataStr); } diff --git a/packages/insomnia-app/app/common/__fixtures__/nestedfolders.js b/packages/insomnia-app/app/common/__fixtures__/nestedfolders.ts similarity index 77% rename from packages/insomnia-app/app/common/__fixtures__/nestedfolders.js rename to packages/insomnia-app/app/common/__fixtures__/nestedfolders.ts index dd0e3be052..a8109640dc 100644 --- a/packages/insomnia-app/app/common/__fixtures__/nestedfolders.js +++ b/packages/insomnia-app/app/common/__fixtures__/nestedfolders.ts @@ -1,14 +1,13 @@ -import * as models from '../../models'; +import { workspace, requestGroup, request, BaseModel } from '../../models'; -export const data = { - [models.workspace.type]: [ +export const data: Record>> = { + [workspace.type]: [ { _id: 'wrk_1', name: 'Wrk 1', }, ], - - [models.requestGroup.type]: [ + [requestGroup.type]: [ { _id: 'fld_1', parentId: 'wrk_1', @@ -25,8 +24,7 @@ export const data = { name: 'Fld 3', }, ], - - [models.request.type]: [ + [request.type]: [ { _id: 'req_1', parentId: 'fld_1', diff --git a/packages/insomnia-app/app/common/__mocks__/analytics.js b/packages/insomnia-app/app/common/__mocks__/analytics.ts similarity index 50% rename from packages/insomnia-app/app/common/__mocks__/analytics.js rename to packages/insomnia-app/app/common/__mocks__/analytics.ts index 39211e48fa..2fc6eabde0 100644 --- a/packages/insomnia-app/app/common/__mocks__/analytics.js +++ b/packages/insomnia-app/app/common/__mocks__/analytics.ts @@ -1,3 +1,4 @@ +// WARNING: changing this to `export default` will break the mock and be incredibly hard to debug. Ask me how I know. const trackEvent = jest.fn(); const trackSegmentEvent = jest.fn(); module.exports = { trackEvent, trackSegmentEvent }; diff --git a/packages/insomnia-app/app/common/__mocks__/render.js b/packages/insomnia-app/app/common/__mocks__/render.js deleted file mode 100644 index 13f1c76d38..0000000000 --- a/packages/insomnia-app/app/common/__mocks__/render.js +++ /dev/null @@ -1,6 +0,0 @@ -const render = jest.requireActual('../render'); - -render.getRenderedGrpcRequest = jest.fn(); -render.getRenderedGrpcRequestMessage = jest.fn(); - -module.exports = render; diff --git a/packages/insomnia-app/app/common/__mocks__/render.ts b/packages/insomnia-app/app/common/__mocks__/render.ts new file mode 100644 index 0000000000..65ce8f9aed --- /dev/null +++ b/packages/insomnia-app/app/common/__mocks__/render.ts @@ -0,0 +1,6 @@ +const _render = jest.requireActual('../render'); +_render.getRenderedGrpcRequest = jest.fn(); +_render.getRenderedGrpcRequestMessage = jest.fn(); + +// WARNING: changing this to `export default` will break the mock and be incredibly hard to debug. Ask me how I know. +module.exports = _render; diff --git a/packages/insomnia-app/app/common/__tests__/analytics.test.js b/packages/insomnia-app/app/common/__tests__/analytics.test.ts similarity index 99% rename from packages/insomnia-app/app/common/__tests__/analytics.test.js rename to packages/insomnia-app/app/common/__tests__/analytics.test.ts index 183dba3256..1efbce36fb 100644 --- a/packages/insomnia-app/app/common/__tests__/analytics.test.js +++ b/packages/insomnia-app/app/common/__tests__/analytics.test.ts @@ -16,9 +16,11 @@ import { describe('init()', () => { beforeEach(async () => { await globalBeforeEach(); - electron.net.request = jest.fn(url => { + electron.net.request = jest.fn(() => { const req = new EventEmitter(); + req.end = function() {}; + return req; }); jest.useFakeTimers(); @@ -31,7 +33,6 @@ describe('init()', () => { }); expect(settings.enableAnalytics).toBe(false); expect(electron.net.request.mock.calls).toEqual([]); - await _trackEvent(true, 'Foo', 'Bar'); jest.runAllTimers(); expect(electron.net.request.mock.calls).toEqual([]); @@ -44,7 +45,6 @@ describe('init()', () => { }); expect(settings.enableAnalytics).toBe(true); expect(electron.net.request.mock.calls).toEqual([]); - await _trackEvent(true, 'Foo', 'Bar'); jest.runAllTimers(); expect(electron.net.request.mock.calls).toEqual([ @@ -78,7 +78,6 @@ describe('init()', () => { deviceId: 'device', enableAnalytics: true, }); - await _trackEvent(false, 'Foo', 'Bar'); jest.runAllTimers(); expect(electron.net.request.mock.calls).toEqual([ @@ -113,7 +112,6 @@ describe('init()', () => { deviceId: 'device', enableAnalytics: true, }); - await _trackPageView('/my/path'); jest.runAllTimers(); expect(electron.net.request.mock.calls).toEqual([ @@ -145,7 +143,6 @@ describe('init()', () => { deviceId: 'device', enableAnalytics: true, }); - await _trackPageView('/my/path'); jest.runAllTimers(); await _trackEvent(true, 'cat', 'act', 'lab', 'val'); diff --git a/packages/insomnia-app/app/common/__tests__/api-specs.test.js b/packages/insomnia-app/app/common/__tests__/api-specs.test.ts similarity index 93% rename from packages/insomnia-app/app/common/__tests__/api-specs.test.js rename to packages/insomnia-app/app/common/__tests__/api-specs.test.ts index 30392032bc..155e0ef4ec 100644 --- a/packages/insomnia-app/app/common/__tests__/api-specs.test.js +++ b/packages/insomnia-app/app/common/__tests__/api-specs.test.ts @@ -8,18 +8,17 @@ describe('parseApiSpec()', () => { it('parses YAML and JSON OpenAPI specs', () => { const objSpec = { openapi: '3.0.0', - info: { title: 'My API' }, + info: { + title: 'My API', + }, }; - const yamlSpec = YAML.stringify(objSpec); const jsonSpec = JSON.stringify(objSpec); - const expected = { format: 'openapi', formatVersion: '3.0.0', contents: objSpec, }; - expect(parseApiSpec(yamlSpec)).toEqual({ ...expected, rawContents: yamlSpec }); expect(parseApiSpec(jsonSpec)).toEqual({ ...expected, rawContents: jsonSpec }); }); @@ -27,18 +26,17 @@ describe('parseApiSpec()', () => { it('parses YAML and JSON Swagger specs', () => { const objSpec = { swagger: '2.0.0', - info: { title: 'My API' }, + info: { + title: 'My API', + }, }; - const expected = { format: 'swagger', formatVersion: '2.0.0', contents: objSpec, }; - const yamlSpec = YAML.stringify(objSpec); const jsonSpec = JSON.stringify(objSpec); - expect(parseApiSpec(yamlSpec)).toEqual({ ...expected, rawContents: yamlSpec }); expect(parseApiSpec(jsonSpec)).toEqual({ ...expected, rawContents: jsonSpec }); }); @@ -46,18 +44,17 @@ describe('parseApiSpec()', () => { it('parses YAML and JSON Unknown specs', () => { const objSpec = { funnyBusiness: '2.0.0', - info: { title: 'My API' }, + info: { + title: 'My API', + }, }; - const expected = { format: null, formatVersion: null, contents: objSpec, }; - const yamlSpec = YAML.stringify(objSpec); const jsonSpec = JSON.stringify(objSpec); - expect(parseApiSpec(yamlSpec)).toEqual({ ...expected, rawContents: yamlSpec }); expect(parseApiSpec(jsonSpec)).toEqual({ ...expected, rawContents: jsonSpec }); }); @@ -69,13 +66,11 @@ describe('parseApiSpec()', () => { contents: null, rawContents: '', }; - expect(parseApiSpec('')).toEqual(expected); }); it('Fails on malformed JSON/YAML', () => { const rawSpec = ['openapi: 3.0.0', 'info: {{{'].join('\n'); - expect(() => parseApiSpec(rawSpec)).toThrowError('Failed to parse API spec'); }); }); diff --git a/packages/insomnia-app/app/common/__tests__/constants.test.js b/packages/insomnia-app/app/common/__tests__/constants.test.ts similarity index 100% rename from packages/insomnia-app/app/common/__tests__/constants.test.js rename to packages/insomnia-app/app/common/__tests__/constants.test.ts diff --git a/packages/insomnia-app/app/common/__tests__/database.test.js b/packages/insomnia-app/app/common/__tests__/database.test.ts similarity index 67% rename from packages/insomnia-app/app/common/__tests__/database.test.js rename to packages/insomnia-app/app/common/__tests__/database.test.ts index e15d9ff447..f830e65bbf 100644 --- a/packages/insomnia-app/app/common/__tests__/database.test.js +++ b/packages/insomnia-app/app/common/__tests__/database.test.ts @@ -1,52 +1,58 @@ import * as models from '../../models'; -import * as db from '../database'; +import { database as db, _repairDatabase } from '../database'; import { globalBeforeEach } from '../../__jest__/before-each'; +import { data as fixtures } from '../__fixtures__/nestedfolders'; -function loadFixture(name) { - const fixtures = require(`../__fixtures__/${name}`).data; - const promises = []; +function loadFixture() { + const promises: Array> = []; for (const type of Object.keys(fixtures)) { for (const doc of fixtures[type]) { - promises.push(db.insert(Object.assign({}, doc, { type }))); + // @ts-expect-error -- TSCONVERSION + promises.push(db.insert({ ...doc, type })); } } - return Promise.all(promises); } describe('init()', () => { beforeEach(globalBeforeEach); + it('handles being initialized twice', async () => { - await db.init(models.types(), { inMemoryOnly: true }); - await db.init(models.types(), { inMemoryOnly: true }); + await db.init(models.types(), { + inMemoryOnly: true, + }); + await db.init(models.types(), { + inMemoryOnly: true, + }); expect((await db.all(models.request.type)).length).toBe(0); }); }); describe('onChange()', () => { beforeEach(globalBeforeEach); + it('handles change listeners', async () => { const doc = { type: models.request.type, parentId: 'nothing', name: 'foo', }; + const changesSeen: Array = []; - const changesSeen = []; const callback = change => { changesSeen.push(change); }; + db.onChange(callback); - const newDoc = await models.request.create(doc); - const updatedDoc = await models.request.update(newDoc, { name: 'bar' }); + const updatedDoc = await models.request.update(newDoc, { + name: 'bar', + }); expect(changesSeen.length).toBe(2); - expect(changesSeen).toEqual([ [[db.CHANGE_INSERT, newDoc, false]], [[db.CHANGE_UPDATE, updatedDoc, false]], ]); - db.offChange(callback); await models.request.create(doc); expect(changesSeen.length).toBe(2); @@ -55,26 +61,26 @@ describe('onChange()', () => { describe('bufferChanges()', () => { beforeEach(globalBeforeEach); + it('properly buffers changes', async () => { const doc = { type: models.request.type, parentId: 'n/a', name: 'foo', }; + const changesSeen: Array = []; - const changesSeen = []; const callback = change => { changesSeen.push(change); }; - db.onChange(callback); + db.onChange(callback); await db.bufferChanges(); const newDoc = await models.request.create(doc); + // @ts-expect-error -- TSCONVERSION appears to be genuine const updatedDoc = await models.request.update(newDoc, true); - // Assert no change seen before flush expect(changesSeen.length).toBe(0); - // Assert changes seen after flush await db.flushChanges(); expect(changesSeen).toEqual([ @@ -83,7 +89,6 @@ describe('bufferChanges()', () => { [db.CHANGE_UPDATE, updatedDoc, false], ], ]); - // Assert no more changes seen after flush again await db.flushChanges(); expect(changesSeen).toEqual([ @@ -100,20 +105,19 @@ describe('bufferChanges()', () => { parentId: 'n/a', name: 'foo', }; + const changesSeen: Array = []; - const changesSeen = []; const callback = change => { changesSeen.push(change); }; - db.onChange(callback); + db.onChange(callback); await db.bufferChanges(); const newDoc = await models.request.create(doc); + // @ts-expect-error -- TSCONVERSION appears to be genuine const updatedDoc = await models.request.update(newDoc, true); - // Default flush timeout is 1000ms after starting buffering await new Promise(resolve => setTimeout(resolve, 1500)); - expect(changesSeen).toEqual([ [ [db.CHANGE_INSERT, newDoc, false], @@ -128,19 +132,18 @@ describe('bufferChanges()', () => { parentId: 'n/a', name: 'foo', }; + const changesSeen: Array = []; - const changesSeen = []; const callback = change => { changesSeen.push(change); }; - db.onChange(callback); + db.onChange(callback); await db.bufferChanges(500); const newDoc = await models.request.create(doc); + // @ts-expect-error -- TSCONVERSION appears to be genuine const updatedDoc = await models.request.update(newDoc, true); - await new Promise(resolve => setTimeout(resolve, 1000)); - expect(changesSeen).toEqual([ [ [db.CHANGE_INSERT, newDoc, false], @@ -152,29 +155,28 @@ describe('bufferChanges()', () => { describe('bufferChangesIndefinitely()', () => { beforeEach(globalBeforeEach); + it('should not auto flush', async () => { const doc = { type: models.request.type, parentId: 'n/a', name: 'foo', }; + const changesSeen: Array = []; - const changesSeen = []; const callback = change => { changesSeen.push(change); }; - db.onChange(callback); + db.onChange(callback); await db.bufferChangesIndefinitely(); const newDoc = await models.request.create(doc); + // @ts-expect-error -- TSCONVERSION appears to be genuine const updatedDoc = await models.request.update(newDoc, true); - // Default flush timeout is 1000ms after starting buffering await new Promise(resolve => setTimeout(resolve, 1500)); - // Assert no change seen before flush expect(changesSeen.length).toBe(0); - // Assert changes seen after flush await db.flushChanges(); expect(changesSeen).toEqual([ @@ -188,17 +190,15 @@ describe('bufferChangesIndefinitely()', () => { describe('requestCreate()', () => { beforeEach(globalBeforeEach); + it('creates a valid request', async () => { const now = Date.now(); - const patch = { name: 'My Request', parentId: 'wrk_123', }; - const r = await models.request.create(patch); expect(Object.keys(r).length).toBe(21); - expect(r._id).toMatch(/^req_[a-zA-Z0-9]{32}$/); expect(r.created).toBeGreaterThanOrEqual(now); expect(r.modified).toBeGreaterThanOrEqual(now); @@ -215,7 +215,11 @@ describe('requestCreate()', () => { }); it('throws when missing parentID', () => { - const fn = () => models.request.create({ name: 'My Request' }); + const fn = () => + models.request.create({ + name: 'My Request', + }); + expect(fn).toThrowError('New Requests missing `parentId`'); }); }); @@ -223,17 +227,20 @@ describe('requestCreate()', () => { describe('requestGroupDuplicate()', () => { beforeEach(async () => { await globalBeforeEach(); - await loadFixture('nestedfolders'); + await loadFixture(); }); it('duplicates a RequestGroup', async () => { const requestGroup = await models.requestGroup.getById('fld_1'); - expect(requestGroup.name).toBe('Fld 1'); + expect(requestGroup).not.toEqual(null); + if (requestGroup === null) { + return; + } + expect(requestGroup.name).toBe('Fld 1'); const newRequestGroup = await models.requestGroup.duplicate(requestGroup); expect(newRequestGroup._id).not.toBe(requestGroup._id); expect(newRequestGroup.name).toBe('Fld 1 (Copy)'); - const allRequests = await models.request.all(); const allRequestGroups = await models.requestGroup.all(); const childRequests = await models.request.findByParentId(requestGroup._id); @@ -244,10 +251,8 @@ describe('requestGroupDuplicate()', () => { // to see that the recursion worked (for the most part) expect(allRequests.length).toBe(8); expect(allRequestGroups.length).toBe(5); - expect(childRequests.length).toBe(2); expect(childRequestGroups.length).toBe(1); - expect(newChildRequests.length).toBe(2); expect(newChildRequestGroups.length).toBe(1); }); @@ -258,190 +263,354 @@ describe('_repairDatabase()', () => { it('fixes duplicate environments', async () => { // Create Workspace with no children - const workspace = await models.workspace.create({ _id: 'w1' }); + const workspace = await models.workspace.create({ + _id: 'w1', + }); const spec = await models.apiSpec.getByParentId(workspace._id); - expect((await db.withDescendants(workspace)).length).toBe(2); - // Create one set of sub environments await models.environment.create({ _id: 'b1', parentId: 'w1', - data: { foo: 'b1', b1: true }, + data: { + foo: 'b1', + b1: true, + }, }); await models.environment.create({ _id: 'b1_sub1', parentId: 'b1', - data: { foo: '1' }, + data: { + foo: '1', + }, }); await models.environment.create({ _id: 'b1_sub2', parentId: 'b1', - data: { foo: '2' }, + data: { + foo: '2', + }, }); - // Create second set of sub environments await models.environment.create({ _id: 'b2', parentId: 'w1', - data: { foo: 'b2', b2: true }, + data: { + foo: 'b2', + b2: true, + }, }); await models.environment.create({ _id: 'b2_sub1', parentId: 'b2', - data: { foo: '3' }, + data: { + foo: '3', + }, }); await models.environment.create({ _id: 'b2_sub2', parentId: 'b2', - data: { foo: '4' }, + data: { + foo: '4', + }, }); - // Make sure we have everything expect((await db.withDescendants(workspace)).length).toBe(8); const descendants = (await db.withDescendants(workspace)).map(d => ({ _id: d._id, parentId: d.parentId, + // @ts-expect-error -- TSCONVERSION appears to be genuine data: d.data || null, })); expect(descendants).toEqual([ - { _id: 'w1', data: null, parentId: null }, - { _id: 'b1', data: { foo: 'b1', b1: true }, parentId: 'w1' }, - { _id: 'b2', data: { foo: 'b2', b2: true }, parentId: 'w1' }, - expect.objectContaining({ _id: spec._id, parentId: 'w1' }), - { _id: 'b1_sub1', data: { foo: '1' }, parentId: 'b1' }, - { _id: 'b1_sub2', data: { foo: '2' }, parentId: 'b1' }, - { _id: 'b2_sub1', data: { foo: '3' }, parentId: 'b2' }, - { _id: 'b2_sub2', data: { foo: '4' }, parentId: 'b2' }, + { + _id: 'w1', + data: null, + parentId: null, + }, + { + _id: 'b1', + data: { + foo: 'b1', + b1: true, + }, + parentId: 'w1', + }, + { + _id: 'b2', + data: { + foo: 'b2', + b2: true, + }, + parentId: 'w1', + }, + expect.objectContaining({ + _id: spec?._id, + parentId: 'w1', + }), + { + _id: 'b1_sub1', + data: { + foo: '1', + }, + parentId: 'b1', + }, + { + _id: 'b1_sub2', + data: { + foo: '2', + }, + parentId: 'b1', + }, + { + _id: 'b2_sub1', + data: { + foo: '3', + }, + parentId: 'b2', + }, + { + _id: 'b2_sub2', + data: { + foo: '4', + }, + parentId: 'b2', + }, ]); // Run the fix algorithm - await db._repairDatabase(); + await _repairDatabase(); // Make sure things get adjusted const descendants2 = (await db.withDescendants(workspace)).map(d => ({ _id: d._id, parentId: d.parentId, + // @ts-expect-error -- TSCONVERSION appears to be genuine data: d.data || null, })); expect(descendants2).toEqual([ - { _id: 'w1', data: null, parentId: null }, - { _id: 'b1', data: { foo: 'b1', b1: true, b2: true }, parentId: 'w1' }, - expect.objectContaining({ _id: spec._id, parentId: 'w1' }), - - // Extra base environments should have been deleted + { + _id: 'w1', + data: null, + parentId: null, + }, + { + _id: 'b1', + data: { + foo: 'b1', + b1: true, + b2: true, + }, + parentId: 'w1', + }, + expect.objectContaining({ + _id: spec?._id, + parentId: 'w1', + }), // Extra base environments should have been deleted // {_id: 'b2', data: {foo: 'bar'}, parentId: 'w1'}, - // Sub environments should have been moved to new "master" base environment - { _id: 'b1_sub1', data: { foo: '1' }, parentId: 'b1' }, - { _id: 'b1_sub2', data: { foo: '2' }, parentId: 'b1' }, - { _id: 'b2_sub1', data: { foo: '3' }, parentId: 'b1' }, - { _id: 'b2_sub2', data: { foo: '4' }, parentId: 'b1' }, + { + _id: 'b1_sub1', + data: { + foo: '1', + }, + parentId: 'b1', + }, + { + _id: 'b1_sub2', + data: { + foo: '2', + }, + parentId: 'b1', + }, + { + _id: 'b2_sub1', + data: { + foo: '3', + }, + parentId: 'b1', + }, + { + _id: 'b2_sub2', + data: { + foo: '4', + }, + parentId: 'b1', + }, ]); }); it('fixes duplicate cookie jars', async () => { // Create Workspace with no children - const workspace = await models.workspace.create({ _id: 'w1' }); + const workspace = await models.workspace.create({ + _id: 'w1', + }); const spec = await models.apiSpec.getByParentId(workspace._id); - expect((await db.withDescendants(workspace)).length).toBe(2); - // Create one set of sub environments await models.cookieJar.create({ _id: 'j1', parentId: 'w1', cookies: [ - { id: '1', key: 'foo', value: '1' }, - { id: 'j1_1', key: 'j1', value: '1' }, + // @ts-expect-error -- TSCONVERSION + { + id: '1', + key: 'foo', + value: '1', + }, + // @ts-expect-error -- TSCONVERSION + { + id: 'j1_1', + key: 'j1', + value: '1', + }, ], }); - await models.cookieJar.create({ _id: 'j2', parentId: 'w1', cookies: [ - { id: '1', key: 'foo', value: '2' }, - { id: 'j2_1', key: 'j2', value: '2' }, + // @ts-expect-error -- TSCONVERSION + { + id: '1', + key: 'foo', + value: '2', + }, + // @ts-expect-error -- TSCONVERSION + { + id: 'j2_1', + key: 'j2', + value: '2', + }, ], }); - // Make sure we have everything expect((await db.withDescendants(workspace)).length).toBe(4); const descendants = (await db.withDescendants(workspace)).map(d => ({ _id: d._id, + // @ts-expect-error -- TSCONVERSION cookies: d.cookies || null, parentId: d.parentId, })); expect(descendants).toEqual([ - { _id: 'w1', cookies: null, parentId: null }, + { + _id: 'w1', + cookies: null, + parentId: null, + }, { _id: 'j1', parentId: 'w1', cookies: [ - { id: '1', key: 'foo', value: '1' }, - { id: 'j1_1', key: 'j1', value: '1' }, + { + id: '1', + key: 'foo', + value: '1', + }, + { + id: 'j1_1', + key: 'j1', + value: '1', + }, ], }, { _id: 'j2', parentId: 'w1', cookies: [ - { id: '1', key: 'foo', value: '2' }, - { id: 'j2_1', key: 'j2', value: '2' }, + { + id: '1', + key: 'foo', + value: '2', + }, + { + id: 'j2_1', + key: 'j2', + value: '2', + }, ], }, - expect.objectContaining({ _id: spec._id, parentId: 'w1' }), + expect.objectContaining({ + _id: spec?._id, + parentId: 'w1', + }), ]); - // Run the fix algorithm - await db._repairDatabase(); - + await _repairDatabase(); // Make sure things get adjusted const descendants2 = (await db.withDescendants(workspace)).map(d => ({ _id: d._id, + // @ts-expect-error -- TSCONVERSION cookies: d.cookies || null, parentId: d.parentId, })); expect(descendants2).toEqual([ - { _id: 'w1', cookies: null, parentId: null }, + { + _id: 'w1', + cookies: null, + parentId: null, + }, { _id: 'j1', parentId: 'w1', cookies: [ - { id: '1', key: 'foo', value: '1' }, - { id: 'j1_1', key: 'j1', value: '1' }, - { id: 'j2_1', key: 'j2', value: '2' }, + { + id: '1', + key: 'foo', + value: '1', + }, + { + id: 'j1_1', + key: 'j1', + value: '1', + }, + { + id: 'j2_1', + key: 'j2', + value: '2', + }, ], }, - expect.objectContaining({ _id: spec._id, parentId: 'w1' }), + expect.objectContaining({ + _id: spec?._id, + parentId: 'w1', + }), ]); }); it('fixes the filename on an apiSpec', async () => { // Create Workspace with apiSpec child (migration in workspace will automatically create this as it is not mocked) - const w1 = await models.workspace.create({ _id: 'w1', name: 'Workspace 1' }); - const w2 = await models.workspace.create({ _id: 'w2', name: 'Workspace 2' }); - const w3 = await models.workspace.create({ _id: 'w3', name: 'Workspace 3' }); - - await models.apiSpec.updateOrCreateForParentId(w1._id, { fileName: '' }); + const w1 = await models.workspace.create({ + _id: 'w1', + name: 'Workspace 1', + }); + const w2 = await models.workspace.create({ + _id: 'w2', + name: 'Workspace 2', + }); + const w3 = await models.workspace.create({ + _id: 'w3', + name: 'Workspace 3', + }); + await models.apiSpec.updateOrCreateForParentId(w1._id, { + fileName: '', + }); await models.apiSpec.updateOrCreateForParentId(w2._id, { fileName: models.apiSpec.init().fileName, }); - await models.apiSpec.updateOrCreateForParentId(w3._id, { fileName: 'Unique name' }); - + await models.apiSpec.updateOrCreateForParentId(w3._id, { + fileName: 'Unique name', + }); // Make sure we have everything - expect((await models.apiSpec.getByParentId(w1._id)).fileName).toBe(''); - expect((await models.apiSpec.getByParentId(w2._id)).fileName).toBe('New Document'); - expect((await models.apiSpec.getByParentId(w3._id)).fileName).toBe('Unique name'); - + expect((await models.apiSpec.getByParentId(w1._id))?.fileName).toBe(''); + expect((await models.apiSpec.getByParentId(w2._id))?.fileName).toBe('New Document'); + expect((await models.apiSpec.getByParentId(w3._id))?.fileName).toBe('Unique name'); // Run the fix algorithm - await db._repairDatabase(); - + await _repairDatabase(); // Make sure things get adjusted - expect((await models.apiSpec.getByParentId(w1._id)).fileName).toBe('Workspace 1'); // Should fix - expect((await models.apiSpec.getByParentId(w2._id)).fileName).toBe('Workspace 2'); // Should fix - expect((await models.apiSpec.getByParentId(w3._id)).fileName).toBe('Unique name'); // should not fix + expect((await models.apiSpec.getByParentId(w1._id))?.fileName).toBe('Workspace 1'); // Should fix + expect((await models.apiSpec.getByParentId(w2._id))?.fileName).toBe('Workspace 2'); // Should fix + expect((await models.apiSpec.getByParentId(w3._id))?.fileName).toBe('Unique name'); // should not fix }); it('fixes old git uris', async () => { @@ -459,9 +628,7 @@ describe('_repairDatabase()', () => { const newRepoWithoutSuffix = await models.gitRepository.create({ uri: 'https://github.com/foo/bar', }); - - await db._repairDatabase(); - + await _repairDatabase(); expect(await db.get(models.gitRepository.type, oldRepoWithSuffix._id)).toEqual( expect.objectContaining({ uri: 'https://github.com/foo/bar.git', @@ -496,18 +663,18 @@ describe('duplicate()', () => { it('should overwrite appropriate fields on the parent when duplicating', async () => { const date = 1478795580200; Date.now = jest.fn().mockReturnValue(date); - const workspace = await models.workspace.create({ name: 'Test Workspace', }); - const newDescription = 'test'; - const duplicated = await db.duplicate(workspace, { description: newDescription }); - + const duplicated = await db.duplicate(workspace, { + description: newDescription, + }); expect(duplicated._id).not.toEqual(workspace._id); expect(duplicated._id).toMatch(/^wrk_[a-z0-9]{32}$/); - + // @ts-expect-error -- TSCONVERSION delete workspace._id; + // @ts-expect-error -- TSCONVERSION delete duplicated._id; expect(duplicated).toEqual({ ...workspace, @@ -517,11 +684,11 @@ describe('duplicate()', () => { type: models.workspace.type, }); }); + it('should should not call migrate when duplicating', async () => { const workspace = await models.workspace.create({ name: 'Test Workspace', }); - const spy = jest.spyOn(models.workspace, 'migrate'); await db.duplicate(workspace); expect(spy).not.toHaveBeenCalled(); @@ -534,11 +701,9 @@ describe('docCreate()', () => { it('should call migrate when creating', async () => { const spy = jest.spyOn(models.workspace, 'migrate'); - await db.docCreate(models.workspace.type, { name: 'Test Workspace', }); - // TODO: This is actually called twice, not once - we should avoid the double model.init() call. expect(spy).toHaveBeenCalled(); }); @@ -549,24 +714,30 @@ describe('withAncestors()', () => { it('should return itself and all parents but exclude siblings', async () => { const wrk = await models.workspace.create(); - const wrkReq = await models.request.create({ parentId: wrk._id }); - const wrkGrpcReq = await models.grpcRequest.create({ parentId: wrk._id }); - const grp = await models.requestGroup.create({ parentId: wrk._id }); - const grpReq = await models.request.create({ parentId: grp._id }); - const grpGrpcReq = await models.grpcRequest.create({ parentId: grp._id }); - + const wrkReq = await models.request.create({ + parentId: wrk._id, + }); + const wrkGrpcReq = await models.grpcRequest.create({ + parentId: wrk._id, + }); + const grp = await models.requestGroup.create({ + parentId: wrk._id, + }); + const grpReq = await models.request.create({ + parentId: grp._id, + }); + const grpGrpcReq = await models.grpcRequest.create({ + parentId: grp._id, + }); // Workspace child searching for ancestors await expect(db.withAncestors(wrk)).resolves.toStrictEqual([wrk]); await expect(db.withAncestors(wrkReq)).resolves.toStrictEqual([wrkReq, wrk]); await expect(db.withAncestors(wrkGrpcReq)).resolves.toStrictEqual([wrkGrpcReq, wrk]); - // Group searching for ancestors await expect(db.withAncestors(grp)).resolves.toStrictEqual([grp, wrk]); - // Group child searching for ancestors await expect(db.withAncestors(grpReq)).resolves.toStrictEqual([grpReq, grp, wrk]); await expect(db.withAncestors(grpGrpcReq)).resolves.toStrictEqual([grpGrpcReq, grp, wrk]); - // Group child searching for ancestors with filters await expect(db.withAncestors(grpGrpcReq, [models.requestGroup.type])).resolves.toStrictEqual([ grpGrpcReq, @@ -575,7 +746,6 @@ describe('withAncestors()', () => { await expect( db.withAncestors(grpGrpcReq, [models.requestGroup.type, models.workspace.type]), ).resolves.toStrictEqual([grpGrpcReq, grp, wrk]); - // Group child searching for ancestors but excluding groups will not find the workspace await expect(db.withAncestors(grpGrpcReq, [models.workspace.type])).resolves.toStrictEqual([ grpGrpcReq, diff --git a/packages/insomnia-app/app/common/__tests__/grpc-paths.test.js b/packages/insomnia-app/app/common/__tests__/grpc-paths.test.ts similarity index 92% rename from packages/insomnia-app/app/common/__tests__/grpc-paths.test.js rename to packages/insomnia-app/app/common/__tests__/grpc-paths.test.ts index 23d3cc0e10..04f0c76cbf 100644 --- a/packages/insomnia-app/app/common/__tests__/grpc-paths.test.js +++ b/packages/insomnia-app/app/common/__tests__/grpc-paths.test.ts @@ -1,5 +1,3 @@ -// @flow - import { getGrpcPathSegments, getShortGrpcPath, @@ -45,9 +43,14 @@ describe('getShortGrpcPath', () => { const serviceName = 'service'; const methodName = 'method'; const fullPath = '/package.service/method'; - - const shortPath = getShortGrpcPath({ packageName, serviceName, methodName }, fullPath); - + const shortPath = getShortGrpcPath( + { + packageName, + serviceName, + methodName, + }, + fullPath, + ); expect(shortPath).toBe('/service/method'); }); @@ -56,13 +59,17 @@ describe('getShortGrpcPath', () => { const serviceName = 'service'; const methodName = 'method'; const fullPath = '/service/method'; - - const shortPath = getShortGrpcPath({ packageName, serviceName, methodName }, fullPath); - + const shortPath = getShortGrpcPath( + { + packageName, + serviceName, + methodName, + }, + fullPath, + ); expect(shortPath).toBe(fullPath); }); }); - const methodBuilder = createBuilder(grpcMethodDefinitionSchema); describe('groupGrpcMethodsByPackage', () => { @@ -87,16 +94,13 @@ describe('groupGrpcMethodsByPackage', () => { .requestStream(true) .responseStream(true) .build(); - const grouped = groupGrpcMethodsByPackage([ packageMethod1, packageMethod2, newPackage, noPackage, ]); - expect(Object.keys(grouped).length).toBe(3); - expect(grouped[NO_PACKAGE_KEY]).toStrictEqual([ { segments: { @@ -108,7 +112,6 @@ describe('groupGrpcMethodsByPackage', () => { fullPath: noPackage.path, }, ]); - expect(grouped.package1).toStrictEqual([ { segments: { @@ -129,7 +132,6 @@ describe('groupGrpcMethodsByPackage', () => { fullPath: packageMethod2.path, }, ]); - expect(grouped.package2).toStrictEqual([ { segments: { diff --git a/packages/insomnia-app/app/common/__tests__/har.test.js b/packages/insomnia-app/app/common/__tests__/har.test.ts similarity index 71% rename from packages/insomnia-app/app/common/__tests__/har.test.js rename to packages/insomnia-app/app/common/__tests__/har.test.ts index 75eb881db7..0bfaf581fd 100644 --- a/packages/insomnia-app/app/common/__tests__/har.test.js +++ b/packages/insomnia-app/app/common/__tests__/har.test.ts @@ -7,6 +7,7 @@ import { globalBeforeEach } from '../../__jest__/before-each'; describe('exportHar()', () => { beforeEach(globalBeforeEach); + it('exports single requests', async () => { const wrk = await models.workspace.create({ _id: 'wrk_1', @@ -16,7 +17,6 @@ describe('exportHar()', () => { _id: 'req_1', name: 'Request 1', parentId: wrk._id, - url: 'https://httpstat.us/200', method: 'POST', body: { @@ -24,26 +24,44 @@ describe('exportHar()', () => { text: '{}', }, headers: [ - { name: 'Content-Type', value: 'application/json' }, - { name: 'Accept', value: 'application/json', disabled: false }, - { name: 'X-Disabled', value: 'X-Disabled', disabled: true }, + { + name: 'Content-Type', + value: 'application/json', + }, + { + name: 'Accept', + value: 'application/json', + disabled: false, + }, + { + name: 'X-Disabled', + value: 'X-Disabled', + disabled: true, + }, ], }); await models.response.create({ parentId: req1._id, - statusCode: 200, statusMessage: 'OK', elapsedTime: 999, - headers: [{ name: 'Content-Type', value: 'application/json' }], + headers: [ + { + name: 'Content-Type', + value: 'application/json', + }, + ], contentType: 'application/json', bodyPath: path.join(__dirname, '../__fixtures__/har/test-response.json'), bodyCompression: null, }); - - const exportRequests = [{ requestId: req1._id, environmentId: 'n/a' }]; + const exportRequests = [ + { + requestId: req1._id, + environmentId: 'n/a', + }, + ]; const harExport = await harUtils.exportHar(exportRequests); - expect(harExport).toMatchObject({ log: { version: '1.2', @@ -60,8 +78,14 @@ describe('exportHar()', () => { httpVersion: 'HTTP/1.1', cookies: [], headers: [ - { name: 'Content-Type', value: 'application/json' }, - { name: 'Accept', value: 'application/json' }, + { + name: 'Content-Type', + value: 'application/json', + }, + { + name: 'Accept', + value: 'application/json', + }, ], queryString: [], postData: { @@ -77,7 +101,12 @@ describe('exportHar()', () => { statusText: 'OK', httpVersion: 'HTTP/1.1', cookies: [], - headers: [{ name: 'Content-Type', value: 'application/json' }], + headers: [ + { + name: 'Content-Type', + value: 'application/json', + }, + ], content: { size: 15, mimeType: 'application/json', @@ -109,34 +138,45 @@ describe('exportHar()', () => { _id: 'wrk_1', name: 'Workspace', }); - const baseReq = await models.request.create({ _id: 'req_0', type: models.request.type, name: 'Request', parentId: workspace._id, - url: 'http://localhost', method: 'GET', body: {}, - headers: [{ name: 'X-Environment', value: '{{ envvalue }}' }], + headers: [ + { + name: 'X-Environment', + value: '{{ envvalue }}', + }, + ], }); const req1 = await models.request.duplicate(baseReq); req1._id = 'req_1'; req1.name = 'Request 1'; - req1.headers.push({ name: 'X-Request', value: '1' }); + req1.headers.push({ + name: 'X-Request', + value: '1', + }); await models.request.create(req1); const req2 = await models.request.duplicate(baseReq); req2._id = 'req_2'; req2.name = 'Request 2'; - req2.headers.push({ name: 'X-Request', value: '2' }); + req2.headers.push({ + name: 'X-Request', + value: '2', + }); await models.request.create(req2); const req3 = await models.request.duplicate(baseReq); req3._id = 'req_3'; req3.name = 'Request 3'; - req3.headers.push({ name: 'X-Request', value: '3' }); + req3.headers.push({ + name: 'X-Request', + value: '3', + }); await models.request.create(req3); - const envBase = await models.environment.getOrCreateForWorkspace(workspace); await models.environment.update(envBase, { data: { @@ -160,7 +200,6 @@ describe('exportHar()', () => { envvalue: 'private', }, }); - await models.response.create({ _id: 'res_1', parentId: req1._id, @@ -176,14 +215,21 @@ describe('exportHar()', () => { parentId: req3._id, statusCode: 500, }); - const exportRequests = [ - { requestId: req1._id, environmentId: null }, - { requestId: req2._id, environmentId: envPublic._id }, - { requestId: req3._id, environmentId: envPrivate._id }, + { + requestId: req1._id, + environmentId: null, + }, + { + requestId: req2._id, + environmentId: envPublic._id, + }, + { + requestId: req3._id, + environmentId: envPrivate._id, + }, ]; const harExport = await harUtils.exportHar(exportRequests); - expect(harExport).toMatchObject({ log: { version: '1.2', @@ -194,8 +240,14 @@ describe('exportHar()', () => { { request: { headers: [ - { name: 'X-Environment', value: '' }, - { name: 'X-Request', value: '1' }, + { + name: 'X-Environment', + value: '', + }, + { + name: 'X-Request', + value: '1', + }, ], }, response: { @@ -206,8 +258,14 @@ describe('exportHar()', () => { { request: { headers: [ - { name: 'X-Environment', value: 'public' }, - { name: 'X-Request', value: '2' }, + { + name: 'X-Environment', + value: 'public', + }, + { + name: 'X-Request', + value: '2', + }, ], }, response: { @@ -218,8 +276,14 @@ describe('exportHar()', () => { { request: { headers: [ - { name: 'X-Environment', value: 'private' }, - { name: 'X-Request', value: '3' }, + { + name: 'X-Environment', + value: 'private', + }, + { + name: 'X-Request', + value: '3', + }, ], }, response: { @@ -235,11 +299,10 @@ describe('exportHar()', () => { describe('exportHarResponse()', () => { beforeEach(globalBeforeEach); + it('exports a default har response for an empty response', async () => { const notFoundResponse = null; - const harResponse = await harUtils.exportHarResponse(notFoundResponse); - expect(harResponse).toMatchObject({ status: 0, statusText: '', @@ -250,6 +313,7 @@ describe('exportHarResponse()', () => { }, }); }); + it('exports a valid har response for a non empty response', async () => { const response = Object.assign(models.response.init(), { _id: 'res_123', @@ -257,20 +321,26 @@ describe('exportHarResponse()', () => { parentId: 'req_123', modified: 0, created: 0, - statusCode: 200, statusMessage: 'OK', headers: [ - { name: 'Content-Type', value: 'application/json' }, - { name: 'Content-Length', value: '2' }, - { name: 'Set-Cookie', value: 'sessionid=12345; HttpOnly; Path=/' }, + { + name: 'Content-Type', + value: 'application/json', + }, + { + name: 'Content-Length', + value: '2', + }, + { + name: 'Set-Cookie', + value: 'sessionid=12345; HttpOnly; Path=/', + }, ], contentType: 'application/json', bodyPath: path.join(__dirname, '../__fixtures__/har/test-response.json'), }); - const harResponse = await harUtils.exportHarResponse(response); - expect(harResponse).toMatchObject({ status: 200, statusText: 'OK', @@ -284,9 +354,18 @@ describe('exportHarResponse()', () => { }, ], headers: [ - { name: 'Content-Type', value: 'application/json' }, - { name: 'Content-Length', value: '2' }, - { name: 'Set-Cookie', value: 'sessionid=12345; HttpOnly; Path=/' }, + { + name: 'Content-Type', + value: 'application/json', + }, + { + name: 'Content-Length', + value: '2', + }, + { + name: 'Set-Cookie', + value: 'sessionid=12345; HttpOnly; Path=/', + }, ], content: { size: 15, @@ -302,6 +381,7 @@ describe('exportHarResponse()', () => { describe('exportHarWithRequest()', () => { beforeEach(globalBeforeEach); + it('renders does it correctly', async () => { const workspace = await models.workspace.create(); const cookies = [ @@ -316,18 +396,26 @@ describe('exportHarWithRequest()', () => { lastAccessed: new Date('2096-10-05T04:40:49.505Z'), }, ]; - const cookieJar = await models.cookieJar.getOrCreateForParentId(workspace._id); await models.cookieJar.update(cookieJar, { parentId: workspace._id, cookies, }); - const request = Object.assign(models.request.init(), { _id: 'req_123', parentId: workspace._id, - headers: [{ name: 'Content-Type', value: 'application/json' }], - parameters: [{ name: 'foo bar', value: 'hello&world' }], + headers: [ + { + name: 'Content-Type', + value: 'application/json', + }, + ], + parameters: [ + { + name: 'foo bar', + value: 'hello&world', + }, + ], method: 'POST', body: { text: 'foo bar', @@ -339,10 +427,8 @@ describe('exportHarWithRequest()', () => { password: 'pass', }, }); - const { request: renderedRequest } = await render.getRenderedRequestAndContext(request); const har = await harUtils.exportHarWithRequest(renderedRequest); - expect(har.cookies.length).toBe(1); expect(har).toEqual({ bodySize: -1, @@ -356,8 +442,14 @@ describe('exportHarWithRequest()', () => { }, ], headers: [ - { name: 'Content-Type', value: 'application/json' }, - { name: 'Authorization', value: 'Basic dXNlcjpwYXNz' }, + { + name: 'Content-Type', + value: 'application/json', + }, + { + name: 'Authorization', + value: 'Basic dXNlcjpwYXNz', + }, ], headersSize: -1, httpVersion: 'HTTP/1.1', @@ -367,7 +459,12 @@ describe('exportHarWithRequest()', () => { params: [], text: 'foo bar', }, - queryString: [{ name: 'foo bar', value: 'hello&world' }], + queryString: [ + { + name: 'foo bar', + value: 'hello&world', + }, + ], url: 'http://google.com/', settingEncodeUrl: true, }); @@ -378,28 +475,49 @@ describe('exportHarWithRequest()', () => { const request = Object.assign(models.request.init(), { _id: 'req_123', parentId: workspace._id, - headers: [{ name: 'Content-Type', value: 'multipart/form-data' }], + headers: [ + { + name: 'Content-Type', + value: 'multipart/form-data', + }, + ], parameters: [], method: 'POST', body: { mimeType: 'multipart/form-data', params: [ - { name: 'a_file', value: '', fileName: '/tmp/my_file', type: 'file' }, - { name: 'a_simple_field', value: 'a_simple_value' }, - { name: 'a_second_file', value: '', fileName: '/tmp/my_file_2', type: 'file' }, + { + name: 'a_file', + value: '', + fileName: '/tmp/my_file', + type: 'file', + }, + { + name: 'a_simple_field', + value: 'a_simple_value', + }, + { + name: 'a_second_file', + value: '', + fileName: '/tmp/my_file_2', + type: 'file', + }, ], }, url: 'http://example.com/post', authentication: {}, }); - const { request: renderedRequest } = await render.getRenderedRequestAndContext(request); const har = await harUtils.exportHarWithRequest(renderedRequest); - expect(har).toEqual({ bodySize: -1, cookies: [], - headers: [{ name: 'Content-Type', value: 'multipart/form-data' }], + headers: [ + { + name: 'Content-Type', + value: 'multipart/form-data', + }, + ], headersSize: -1, httpVersion: 'HTTP/1.1', method: 'POST', diff --git a/packages/insomnia-app/app/common/__tests__/import.test.js b/packages/insomnia-app/app/common/__tests__/import.test.ts similarity index 72% rename from packages/insomnia-app/app/common/__tests__/import.test.js rename to packages/insomnia-app/app/common/__tests__/import.test.ts index 5eba1f3429..2c8b9682d1 100644 --- a/packages/insomnia-app/app/common/__tests__/import.test.js +++ b/packages/insomnia-app/app/common/__tests__/import.test.ts @@ -1,5 +1,3 @@ -// @flow - import * as models from '../../models'; import * as importUtil from '../import'; import { getAppVersion } from '../constants'; @@ -8,6 +6,7 @@ import YAML from 'yaml'; describe('exportWorkspacesHAR() and exportRequestsHAR()', () => { beforeEach(globalBeforeEach); + it('exports a single workspace and some requests only as an HTTP Archive', async () => { const wrk1 = await models.workspace.create({ _id: 'wrk_1', @@ -17,7 +16,12 @@ describe('exportWorkspacesHAR() and exportRequestsHAR()', () => { _id: 'req_1', name: 'Request 1', parentId: wrk1._id, - headers: [{ name: 'X-Environment', value: '{{ envvalue }}' }], + headers: [ + { + name: 'X-Environment', + value: '{{ envvalue }}', + }, + ], metaSortKey: 0, }); const req2 = await models.request.create({ @@ -43,7 +47,6 @@ describe('exportWorkspacesHAR() and exportRequestsHAR()', () => { parentId: wrk1._id, activeEnvironmentId: env1Private._id, }); - const wrk2 = await models.workspace.create({ _id: 'wrk_2', name: 'Workspace 2', @@ -53,9 +56,7 @@ describe('exportWorkspacesHAR() and exportRequestsHAR()', () => { name: 'Request 3', parentId: wrk2._id, }); - const includePrivateDocs = true; - // Test export whole workspace. const exportWorkspacesJson = await importUtil.exportWorkspacesHAR(wrk1, includePrivateDocs); const exportWorkspacesData = JSON.parse(exportWorkspacesJson); @@ -64,7 +65,12 @@ describe('exportWorkspacesHAR() and exportRequestsHAR()', () => { entries: [ { request: { - headers: [{ name: 'X-Environment', value: 'private1' }], + headers: [ + { + name: 'X-Environment', + value: 'private1', + }, + ], }, comment: req1.name, }, @@ -75,7 +81,6 @@ describe('exportWorkspacesHAR() and exportRequestsHAR()', () => { }, }); expect(exportWorkspacesData.log.entries.length).toBe(2); - // Test export some requests only. const exportRequestsJson = await importUtil.exportRequestsHAR([req1], includePrivateDocs); const exportRequestsData = JSON.parse(exportRequestsJson); @@ -84,7 +89,12 @@ describe('exportWorkspacesHAR() and exportRequestsHAR()', () => { entries: [ { request: { - headers: [{ name: 'X-Environment', value: 'private1' }], + headers: [ + { + name: 'X-Environment', + value: 'private1', + }, + ], }, comment: req1.name, }, @@ -93,6 +103,7 @@ describe('exportWorkspacesHAR() and exportRequestsHAR()', () => { }); expect(exportRequestsData.log.entries.length).toBe(1); }); + it('exports all workspaces as an HTTP Archive', async () => { const wrk1 = await models.workspace.create({ _id: 'wrk_1', @@ -106,15 +117,24 @@ describe('exportWorkspacesHAR() and exportRequestsHAR()', () => { _id: 'req_1', name: 'Request 1', parentId: wrk1._id, - headers: [{ name: 'X-Environment', value: '{{ envvalue }}' }], + headers: [ + { + name: 'X-Environment', + value: '{{ envvalue }}', + }, + ], }); await models.request.create({ _id: 'req_2', name: 'Request 2', parentId: wrk2._id, - headers: [{ name: 'X-Environment', value: '{{ envvalue }}' }], + headers: [ + { + name: 'X-Environment', + value: '{{ envvalue }}', + }, + ], }); - let env1Base = await models.environment.getOrCreateForWorkspace(wrk1); env1Base = await models.environment.update(env1Base, { data: { @@ -142,7 +162,6 @@ describe('exportWorkspacesHAR() and exportRequestsHAR()', () => { envvalue: 'private2', }, }); - await models.workspaceMeta.create({ parentId: wrk1._id, activeEnvironmentId: env1Public._id, @@ -151,23 +170,31 @@ describe('exportWorkspacesHAR() and exportRequestsHAR()', () => { parentId: wrk2._id, activeEnvironmentId: env2Private._id, }); - const includePrivateDocs = false; const json = await importUtil.exportWorkspacesHAR(null, includePrivateDocs); const data = JSON.parse(json); - expect(data).toMatchObject({ log: { entries: expect.arrayContaining([ expect.objectContaining({ request: expect.objectContaining({ - headers: [{ name: 'X-Environment', value: 'public1' }], + headers: [ + { + name: 'X-Environment', + value: 'public1', + }, + ], }), comment: 'Request 1', }), expect.objectContaining({ request: expect.objectContaining({ - headers: [{ name: 'X-Environment', value: 'base2' }], + headers: [ + { + name: 'X-Environment', + value: 'base2', + }, + ], }), comment: 'Request 2', }), @@ -179,9 +206,13 @@ describe('exportWorkspacesHAR() and exportRequestsHAR()', () => { describe('export', () => { beforeEach(globalBeforeEach); + it('exports all workspaces and some requests only', async () => { - const w = await models.workspace.create({ name: 'Workspace' }); + const w = await models.workspace.create({ + name: 'Workspace', + }); const spec = await models.apiSpec.getByParentId(w._id); // Created by workspace migration + const jar = await models.cookieJar.getOrCreateForParentId(w._id); const r1 = await models.request.create({ name: 'Request 1', @@ -227,70 +258,108 @@ describe('export', () => { isPrivate: true, parentId: eBase._id, }); - // Test export whole workspace. const exportedWorkspacesJson = await importUtil.exportWorkspacesData(null, false, 'json'); const exportedWorkspacesYaml = await importUtil.exportWorkspacesData(null, false, 'yaml'); const exportWorkspacesDataJson = JSON.parse(exportedWorkspacesJson); const exportWorkspacesDataYaml = YAML.parse(exportedWorkspacesYaml); - // Ensure JSON is the same as YAML expect(exportWorkspacesDataJson.resources).toEqual(exportWorkspacesDataYaml.resources); - expect(exportWorkspacesDataJson).toMatchObject({ _type: 'export', __export_format: 4, __export_date: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/), __export_source: `insomnia.desktop.app:v${getAppVersion()}`, resources: expect.arrayContaining([ - expect.objectContaining({ _id: w._id }), - expect.objectContaining({ _id: spec._id }), - expect.objectContaining({ _id: eBase._id }), - expect.objectContaining({ _id: jar._id }), - expect.objectContaining({ _id: r1._id }), - expect.objectContaining({ _id: f2._id }), - expect.objectContaining({ _id: r2._id }), - expect.objectContaining({ _id: ePub._id }), - expect.objectContaining({ _id: gr1._id }), - expect.objectContaining({ _id: pd._id }), - expect.objectContaining({ _id: pf1._id }), - expect.objectContaining({ _id: gr2._id }), - expect.objectContaining({ _id: pf2._id }), + expect.objectContaining({ + _id: w._id, + }), + expect.objectContaining({ + _id: spec._id, + }), + expect.objectContaining({ + _id: eBase._id, + }), + expect.objectContaining({ + _id: jar._id, + }), + expect.objectContaining({ + _id: r1._id, + }), + expect.objectContaining({ + _id: f2._id, + }), + expect.objectContaining({ + _id: r2._id, + }), + expect.objectContaining({ + _id: ePub._id, + }), + expect.objectContaining({ + _id: gr1._id, + }), + expect.objectContaining({ + _id: pd._id, + }), + expect.objectContaining({ + _id: pf1._id, + }), + expect.objectContaining({ + _id: gr2._id, + }), + expect.objectContaining({ + _id: pf2._id, + }), ]), }); expect(exportWorkspacesDataJson.resources.length).toBe(13); - // Test export some requests only. const exportRequestsJson = await importUtil.exportRequestsData([r1, gr1], false, 'json'); const exportRequestsYaml = await importUtil.exportRequestsData([r1, gr1], false, 'yaml'); const exportRequestsDataJSON = JSON.parse(exportRequestsJson); const exportRequestsDataYAML = YAML.parse(exportRequestsYaml); - expect(exportRequestsDataJSON).toMatchObject({ _type: 'export', __export_format: 4, __export_date: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/), __export_source: `insomnia.desktop.app:v${getAppVersion()}`, resources: expect.arrayContaining([ - expect.objectContaining({ _id: w._id }), - expect.objectContaining({ _id: eBase._id }), - expect.objectContaining({ _id: jar._id }), - expect.objectContaining({ _id: r1._id }), - expect.objectContaining({ _id: ePub._id }), - expect.objectContaining({ _id: gr1._id }), - expect.objectContaining({ _id: pf1._id }), - expect.objectContaining({ _id: pf2._id }), + expect.objectContaining({ + _id: w._id, + }), + expect.objectContaining({ + _id: eBase._id, + }), + expect.objectContaining({ + _id: jar._id, + }), + expect.objectContaining({ + _id: r1._id, + }), + expect.objectContaining({ + _id: ePub._id, + }), + expect.objectContaining({ + _id: gr1._id, + }), + expect.objectContaining({ + _id: pf1._id, + }), + expect.objectContaining({ + _id: pf2._id, + }), ]), }); - expect(exportRequestsDataJSON.resources.length).toBe(10); expect(exportRequestsDataYAML.resources.length).toBe(10); - // Ensure JSON and YAML are the same expect(exportRequestsDataJSON.resources).toEqual(exportRequestsDataYAML.resources); }); + it('exports correct models', async () => { - const w = await models.workspace.create({ name: 'Workspace' }); + const w = await models.workspace.create({ + name: 'Workspace', + }); const spec = await models.apiSpec.getOrCreateForParentId(w._id, { type: 'yaml', contents: 'openapi: "3.0.0"', @@ -348,29 +417,55 @@ describe('export', () => { isPrivate: true, parentId: eBase._id, }); - const result = await importUtil.exportWorkspacesData(w, false, 'json'); - expect(JSON.parse(result)).toEqual({ _type: 'export', __export_format: 4, __export_date: expect.stringMatching(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d{3}Z$/), __export_source: `insomnia.desktop.app:v${getAppVersion()}`, resources: expect.arrayContaining([ - expect.objectContaining({ _id: w._id }), - expect.objectContaining({ _id: eBase._id }), - expect.objectContaining({ _id: jar._id }), - expect.objectContaining({ _id: pd._id }), - expect.objectContaining({ _id: pf1._id }), - expect.objectContaining({ _id: pf2._id }), - expect.objectContaining({ _id: r1._id }), - expect.objectContaining({ _id: r2._id }), - expect.objectContaining({ _id: gr1._id }), - expect.objectContaining({ _id: gr2._id }), - expect.objectContaining({ _id: uts1._id }), - expect.objectContaining({ _id: ut1._id }), - expect.objectContaining({ _id: ePub._id }), - expect.objectContaining({ _id: spec._id }), + expect.objectContaining({ + _id: w._id, + }), + expect.objectContaining({ + _id: eBase._id, + }), + expect.objectContaining({ + _id: jar._id, + }), + expect.objectContaining({ + _id: pd._id, + }), + expect.objectContaining({ + _id: pf1._id, + }), + expect.objectContaining({ + _id: pf2._id, + }), + expect.objectContaining({ + _id: r1._id, + }), + expect.objectContaining({ + _id: r2._id, + }), + expect.objectContaining({ + _id: gr1._id, + }), + expect.objectContaining({ + _id: gr2._id, + }), + expect.objectContaining({ + _id: uts1._id, + }), + expect.objectContaining({ + _id: ut1._id, + }), + expect.objectContaining({ + _id: ePub._id, + }), + expect.objectContaining({ + _id: spec._id, + }), ]), }); }); @@ -378,24 +473,38 @@ describe('export', () => { describe('isApiSpecImport()', () => { it.each(['swagger2', 'openapi3'])('should return true if spec id is %o', (id: string) => { - expect(importUtil.isApiSpecImport({ id })).toBe(true); + expect( + importUtil.isApiSpecImport({ + id, + }), + ).toBe(true); }); it('should return false if spec id is not valid', () => { const id = 'invalid-id'; - - expect(importUtil.isApiSpecImport({ id })).toBe(false); + expect( + importUtil.isApiSpecImport({ + id, + }), + ).toBe(false); }); }); describe('isInsomniaV4Import()', () => { it.each(['insomnia-4'])('should return true if spec id is %o', (id: string) => { - expect(importUtil.isInsomniaV4Import({ id })).toBe(true); + expect( + importUtil.isInsomniaV4Import({ + id, + }), + ).toBe(true); }); it('should return false if spec id is not valid', () => { const id = 'invalid-id'; - - expect(importUtil.isInsomniaV4Import({ id })).toBe(false); + expect( + importUtil.isInsomniaV4Import({ + id, + }), + ).toBe(false); }); }); diff --git a/packages/insomnia-app/app/common/__tests__/local-storage.test.js b/packages/insomnia-app/app/common/__tests__/local-storage.test.ts similarity index 93% rename from packages/insomnia-app/app/common/__tests__/local-storage.test.js rename to packages/insomnia-app/app/common/__tests__/local-storage.test.ts index e21a4f2871..736ee5a706 100644 --- a/packages/insomnia-app/app/common/__tests__/local-storage.test.js +++ b/packages/insomnia-app/app/common/__tests__/local-storage.test.ts @@ -7,7 +7,6 @@ describe('LocalStorage()', () => { beforeEach(async () => { await globalBeforeEach(); jest.useFakeTimers(); - // There has to be a better way to reset this... setTimeout.mock.calls = []; }); @@ -27,16 +26,19 @@ describe('LocalStorage()', () => { it('does basic operations', () => { const basePath = `/tmp/insomnia-localstorage-${Math.random()}`; const localStorage = new LocalStorage(basePath); - // Test get and set localStorage.setItem('foo', 'bar 1'); localStorage.setItem('foo', 'bar'); expect(localStorage.getItem('foo', 'BAD')).toBe('bar'); - // Test Object storage - localStorage.setItem('obj', { foo: 'bar', arr: [1, 2, 3] }); - expect(localStorage.getItem('obj')).toEqual({ foo: 'bar', arr: [1, 2, 3] }); - + localStorage.setItem('obj', { + foo: 'bar', + arr: [1, 2, 3], + }); + expect(localStorage.getItem('obj')).toEqual({ + foo: 'bar', + arr: [1, 2, 3], + }); // Test default values expect(localStorage.getItem('dne', 'default')).toEqual('default'); expect(localStorage.getItem('dne')).toEqual('default'); @@ -44,30 +46,26 @@ describe('LocalStorage()', () => { it('does handles malformed files', () => { const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - const basePath = `/tmp/insomnia-localstorage-${Math.random()}`; const localStorage = new LocalStorage(basePath); - // Assert default is returned on bad JSON fs.writeFileSync(path.join(basePath, 'key'), '{bad JSON'); expect(localStorage.getItem('key', 'default')).toBe('default'); - // Assert that writing our file actually works fs.writeFileSync(path.join(basePath, 'key'), '{"good": "JSON"}'); - expect(localStorage.getItem('key', 'default')).toEqual({ good: 'JSON' }); + expect(localStorage.getItem('key', 'default')).toEqual({ + good: 'JSON', + }); expect(consoleErrorSpy).toHaveBeenCalled(); }); it('does handles failing to write file', () => { const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); - const basePath = `/tmp/insomnia-localstorage-${Math.random()}`; const localStorage = new LocalStorage(basePath); fs.rmdirSync(basePath); localStorage.setItem('key', 'value'); - jest.runAllTimers(); - // Since the above operation failed to write, we should now get back // the default value expect(localStorage.getItem('key', 'different')).toBe('different'); @@ -78,17 +76,13 @@ describe('LocalStorage()', () => { const basePath = `/tmp/insomnia-localstorage-${Math.random()}`; const localStorage = new LocalStorage(basePath); localStorage.setItem('foo', 'bar'); - // Assert timeouts are called expect(setTimeout.mock.calls.length).toBe(1); expect(setTimeout.mock.calls[0][1]).toBe(100); - // Force debouncer to flush jest.runAllTimers(); - // Assert there is one item stored expect(fs.readdirSync(basePath).length).toEqual(1); - // Assert the contents are correct const contents = fs.readFileSync(path.join(basePath, 'foo'), 'utf8'); expect(contents).toEqual('"bar"'); @@ -100,18 +94,14 @@ describe('LocalStorage()', () => { localStorage.setItem('foo', 'bar1'); localStorage.setItem('another', 10); localStorage.setItem('foo', 'bar3'); - // Assert timeouts are called expect(setTimeout.mock.calls.length).toBe(3); expect(setTimeout.mock.calls[0][1]).toBe(100); expect(setTimeout.mock.calls[1][1]).toBe(100); expect(setTimeout.mock.calls[2][1]).toBe(100); - expect(fs.readdirSync(basePath).length).toEqual(0); - // Force flush jest.runAllTimers(); - // Make sure only one item exists expect(fs.readdirSync(basePath).length).toEqual(2); expect(fs.readFileSync(path.join(basePath, 'foo'), 'utf8')).toEqual('"bar3"'); diff --git a/packages/insomnia-app/app/common/__tests__/misc.test.js b/packages/insomnia-app/app/common/__tests__/misc.test.ts similarity index 77% rename from packages/insomnia-app/app/common/__tests__/misc.test.js rename to packages/insomnia-app/app/common/__tests__/misc.test.ts index 48d6fd705d..9521ae0188 100644 --- a/packages/insomnia-app/app/common/__tests__/misc.test.js +++ b/packages/insomnia-app/app/common/__tests__/misc.test.ts @@ -10,27 +10,39 @@ import { describe('hasAuthHeader()', () => { beforeEach(globalBeforeEach); + it('finds valid header', () => { const yes = misc.hasAuthHeader([ - { name: 'foo', value: 'bar' }, - { name: 'authorization', value: 'foo' }, + { + name: 'foo', + value: 'bar', + }, + { + name: 'authorization', + value: 'foo', + }, ]); - expect(yes).toEqual(true); }); it('finds valid header case insensitive', () => { const yes = misc.hasAuthHeader([ - { name: 'foo', value: 'bar' }, - { name: 'AuthOrizAtiOn', value: 'foo' }, + { + name: 'foo', + value: 'bar', + }, + { + name: 'AuthOrizAtiOn', + value: 'foo', + }, ]); - expect(yes).toEqual(true); }); }); describe('generateId()', () => { beforeEach(globalBeforeEach); + it('generates a valid ID', () => { const id = misc.generateId('foo'); expect(id).toMatch(/^foo_[a-z0-9]{32}$/); @@ -44,18 +56,72 @@ describe('generateId()', () => { describe('filterHeaders()', () => { beforeEach(globalBeforeEach); + it('handles bad headers', () => { expect(misc.filterHeaders(null, null)).toEqual([]); expect(misc.filterHeaders([], null)).toEqual([]); expect(misc.filterHeaders(['bad'], null)).toEqual([]); expect(misc.filterHeaders(['bad'], 'good')).toEqual([]); expect(misc.filterHeaders(null, 'good')).toEqual([]); - expect(misc.filterHeaders([{ name: '', value: 'valid' }], '')).toEqual([]); - expect(misc.filterHeaders([{ name: 123, value: 123 }], 123)).toEqual([]); - expect(misc.filterHeaders([{ name: 'good', value: 'valid' }], 123)).toEqual([]); - expect(misc.filterHeaders([{ name: 'good', value: 'valid' }], null)).toEqual([]); - expect(misc.filterHeaders([{ name: 'good', value: 'valid' }], 'good')).toEqual([ - { name: 'good', value: 'valid' }, + expect( + misc.filterHeaders( + [ + { + name: '', + value: 'valid', + }, + ], + '', + ), + ).toEqual([]); + expect( + misc.filterHeaders( + [ + { + name: 123, + value: 123, + }, + ], + 123, + ), + ).toEqual([]); + expect( + misc.filterHeaders( + [ + { + name: 'good', + value: 'valid', + }, + ], + 123, + ), + ).toEqual([]); + expect( + misc.filterHeaders( + [ + { + name: 'good', + value: 'valid', + }, + ], + null, + ), + ).toEqual([]); + expect( + misc.filterHeaders( + [ + { + name: 'good', + value: 'valid', + }, + ], + 'good', + ), + ).toEqual([ + { + name: 'good', + value: 'valid', + }, ]); }); }); @@ -71,18 +137,14 @@ describe('keyedDebounce()', () => { const fn = misc.keyedDebounce(results => { resultsList.push(results); }, 100); - fn('foo', 'bar'); fn('baz', 'bar'); fn('foo', 'bar2'); fn('foo', 'bar3'); fn('multi', 'foo', 'bar', 'baz'); - expect(setTimeout.mock.calls.length).toBe(5); expect(resultsList).toEqual([]); - jest.runAllTimers(); - expect(resultsList).toEqual([ { foo: ['bar3'], @@ -104,31 +166,27 @@ describe('debounce()', () => { const fn = misc.debounce((...args) => { resultList.push(args); }, 100); - fn('foo'); fn('foo'); fn('multi', 'foo', 'bar', 'baz'); fn('baz', 'bar'); fn('foo', 'bar3'); - expect(setTimeout.mock.calls.length).toBe(5); expect(resultList).toEqual([]); - jest.runAllTimers(); - expect(resultList).toEqual([['foo', 'bar3']]); }); }); describe('fuzzyMatch()', () => { beforeEach(globalBeforeEach); + it('can get a positive fuzzy match on a single field', () => { expect(misc.fuzzyMatch('test', 'testing')).toEqual({ score: -3, indexes: [0, 1, 2, 3], target: 'testing', }); - expect(misc.fuzzyMatch('tst', 'testing')).toEqual({ score: -2004, indexes: [0, 2, 3], @@ -144,6 +202,7 @@ describe('fuzzyMatch()', () => { describe('fuzzyMatchAll()', () => { beforeEach(globalBeforeEach); + it('can get a positive fuzzy match on multiple fields', () => { expect(misc.fuzzyMatchAll('', [undefined])).toEqual(null); expect(misc.fuzzyMatchAll('', ['testing'])).toEqual(null); @@ -153,7 +212,11 @@ describe('fuzzyMatchAll()', () => { indexes: [0, 1, 2, 3], target: 'testing foo', }); - expect(misc.fuzzyMatchAll('test foo', ['testing', 'foo'], { splitSpace: true })).toEqual({ + expect( + misc.fuzzyMatchAll('test foo', ['testing', 'foo'], { + splitSpace: true, + }), + ).toEqual({ score: 0, indexes: [0, 1, 2, 3, 0, 1, 2], target: 'testing foo', @@ -225,32 +288,65 @@ describe('pluralize()', () => { }); describe('diffPatchObj()', () => { - const a = { x: 1 }; - const b = { x: 2, y: 3 }; - const c = { x: 4, y: { z: 5 } }; + const a = { + x: 1, + }; + const b = { + x: 2, + y: 3, + }; + const c = { + x: 4, + y: { + z: 5, + }, + }; it('does a basic merge', () => { - expect(diffPatchObj(a, b)).toEqual({ x: 2, y: 3 }); - - expect(diffPatchObj(b, a)).toEqual({ x: 1, y: 3 }); + expect(diffPatchObj(a, b)).toEqual({ + x: 2, + y: 3, + }); + expect(diffPatchObj(b, a)).toEqual({ + x: 1, + y: 3, + }); }); it.skip('does a basic merge, deep', () => { - expect(diffPatchObj(a, c, true)).toEqual({ x: 2, y: 3 }); - - expect(diffPatchObj(c, a, true)).toEqual({ x: 1 }); + expect(diffPatchObj(a, c, true)).toEqual({ + x: 2, + y: 3, + }); + expect(diffPatchObj(c, a, true)).toEqual({ + x: 1, + }); }); it.skip('does a basic nested merge', () => { - expect(diffPatchObj(a, b)).toEqual({ x: 2, y: 3 }); - - expect(diffPatchObj(b, a)).toEqual({ x: 1, y: { z: 5 } }); + expect(diffPatchObj(a, b)).toEqual({ + x: 2, + y: 3, + }); + expect(diffPatchObj(b, a)).toEqual({ + x: 1, + y: { + z: 5, + }, + }); }); it.skip('does a basic nested merge, deep', () => { - expect(diffPatchObj(a, c, true)).toEqual({ x: 2, y: 3 }); - - expect(diffPatchObj(c, a, true)).toEqual({ x: 1, y: { z: 5 } }); + expect(diffPatchObj(a, c, true)).toEqual({ + x: 2, + y: 3, + }); + expect(diffPatchObj(c, a, true)).toEqual({ + x: 1, + y: { + z: 5, + }, + }); }); }); @@ -297,7 +393,6 @@ describe('isNotNullOrUndefined', () => { expect(isNotNullOrUndefined(0)).toBe(true); expect(isNotNullOrUndefined('')).toBe(true); expect(isNotNullOrUndefined(false)).toBe(true); - expect(isNotNullOrUndefined(null)).toBe(false); expect(isNotNullOrUndefined(undefined)).toBe(false); }); diff --git a/packages/insomnia-app/app/common/__tests__/render.test.js b/packages/insomnia-app/app/common/__tests__/render.test.ts similarity index 94% rename from packages/insomnia-app/app/common/__tests__/render.test.js rename to packages/insomnia-app/app/common/__tests__/render.test.ts index b14535fef1..b506368274 100644 --- a/packages/insomnia-app/app/common/__tests__/render.test.js +++ b/packages/insomnia-app/app/common/__tests__/render.test.ts @@ -6,6 +6,7 @@ jest.mock('electron'); describe('render()', () => { beforeEach(globalBeforeEach); + it('renders hello world', async () => { const rendered = await renderUtils.render('Hello {{ msg }}!', { msg: 'World', @@ -25,14 +26,20 @@ describe('render()', () => { it('renders nested object', async () => { const rendered = await renderUtils.render('Hello {{ users[0].name }}!', { - users: [{ name: 'Niji' }], + users: [ + { + name: 'Niji', + }, + ], }); expect(rendered).toBe('Hello Niji!'); }); it('fails on invalid template', async () => { try { - await renderUtils.render('Hello {{ msg }!', { msg: 'World' }); + await renderUtils.render('Hello {{ msg }!', { + msg: 'World', + }); fail('Render should have failed'); } catch (err) { expect(err.message).toContain('expected variable end'); @@ -52,16 +59,13 @@ describe('render()', () => { '&': ['value', 'replaced', 'hashed', 'consume'], }, }; - const context = await renderUtils.buildRenderContext([], rootEnvironment); - expect(context).toEqual({ value: 'ThisIsATopSecretValue', hashed: 'f67565de946a899a534fd908e7eef872', replaced: 'f67565de946a899a534fd908e7eef872', consume: 'f67565de946a899a534fd908e7eef872', }); - // In runtime, this context is used to render, which re-evaluates the expression for replaced in the rootEnvironment by using the built context // Regression test from issue 1917 - https://github.com/Kong/insomnia/issues/1917 const renderExpression = await renderUtils.render(rootEnvironment.data.replaced, context); @@ -71,6 +75,7 @@ describe('render()', () => { describe('buildRenderContext()', () => { beforeEach(globalBeforeEach); + it('cascades properly', async () => { const ancestors = [ { @@ -88,7 +93,6 @@ describe('buildRenderContext()', () => { }, }, ]; - const rootEnvironment = { type: models.environment.type, data: { @@ -96,7 +100,6 @@ describe('buildRenderContext()', () => { root: true, }, }; - const subEnvironment = { type: models.environment.type, data: { @@ -104,13 +107,11 @@ describe('buildRenderContext()', () => { sub: true, }, }; - const context = await renderUtils.buildRenderContext( ancestors, rootEnvironment, subEnvironment, ); - expect(context).toEqual({ foo: 'parent', ancestor: true, @@ -124,12 +125,12 @@ describe('buildRenderContext()', () => { { // Sub Environment type: models.requestGroup.type, - environment: { recursive: '{{ recursive }}/hello' }, + environment: { + recursive: '{{ recursive }}/hello', + }, }, ]; - const context = await renderUtils.buildRenderContext(ancestors); - // This is longer than 3 because it multiplies every time (1 -> 2 -> 4 -> 8) expect(context).toEqual({ recursive: '{{ recursive }}/hello/hello/hello/hello/hello/hello/hello/hello', @@ -145,7 +146,6 @@ describe('buildRenderContext()', () => { url: '{{ proto }}://{{ domain }}', }, }; - const sub = { type: models.environment.type, data: { @@ -155,7 +155,6 @@ describe('buildRenderContext()', () => { url: '{{ proto }}://{{ domain }}:{{ port }}', }, }; - const ancestors = [ { // Folder Environment @@ -167,9 +166,7 @@ describe('buildRenderContext()', () => { }, }, ]; - const context = await renderUtils.buildRenderContext(ancestors, root, sub); - expect(context).toEqual({ proto: 'https', domain: 'folder.com', @@ -181,14 +178,16 @@ describe('buildRenderContext()', () => { it('does the thing', async () => { const root = { type: models.environment.type, - data: { url: 'insomnia.rest' }, + data: { + url: 'insomnia.rest', + }, }; - const sub = { type: models.environment.type, - data: { url: '{{ url }}/sub' }, + data: { + url: '{{ url }}/sub', + }, }; - const ancestors = [ { // Folder Environment @@ -199,9 +198,7 @@ describe('buildRenderContext()', () => { }, }, ]; - const context = await renderUtils.buildRenderContext(ancestors, root, sub); - expect(context).toEqual({ url: 'insomnia.rest/sub/folder', name: 'folder', @@ -222,9 +219,7 @@ describe('buildRenderContext()', () => { }, }, ]; - const context = await renderUtils.buildRenderContext(ancestors); - expect(context).toEqual({ d: '/d', c: '/c/d', @@ -245,9 +240,7 @@ describe('buildRenderContext()', () => { }, }, ]; - const context = await renderUtils.buildRenderContext(ancestors); - expect(context).toEqual({ sibling: 'sibling', test: 'sibling/hello', @@ -271,9 +264,7 @@ describe('buildRenderContext()', () => { }, }, ]; - const context = await renderUtils.buildRenderContext(ancestors); - expect(context).toEqual({ grandparent: 'grandparent', test: 'grandparent parent', @@ -297,10 +288,10 @@ describe('buildRenderContext()', () => { }, }, ]; - const context = await renderUtils.buildRenderContext(ancestors); - - expect(context).toEqual({ base_url: 'https://insomnia.rest/resource' }); + expect(context).toEqual({ + base_url: 'https://insomnia.rest/resource', + }); }); it('rendered parent, ignoring sibling environment variables', async () => { @@ -329,7 +320,6 @@ describe('buildRenderContext()', () => { }, }, ]; - const context = await renderUtils.buildRenderContext(ancestors); expect(await renderUtils.render('{{ urls.admin }}/foo', context)).toBe( 'https://parent.com/admin/foo', @@ -356,9 +346,7 @@ describe('buildRenderContext()', () => { }, }, ]; - const context = await renderUtils.buildRenderContext(ancestors); - expect(context).toEqual({ parent: 'parent', test: 'parent grandparent', @@ -376,23 +364,41 @@ describe('buildRenderContext()', () => { name: 'Grandparent', type: models.requestGroup.type, environment: { - users: [{ name: 'Mike' }, { name: 'Opender' }], + users: [ + { + name: 'Mike', + }, + { + name: 'Opender', + }, + ], }, }, ]; - const context = await renderUtils.buildRenderContext(ancestors); - expect(context).toEqual({ - users: [{ name: 'Mike' }, { name: 'Opender' }], + users: [ + { + name: 'Mike', + }, + { + name: 'Opender', + }, + ], }); }); it('works with ordered objects', async () => { const obj = { users: [ - { name: 'Mike', id: 1 }, - { name: 'Opender', id: 2 }, + { + name: 'Mike', + id: 1, + }, + { + name: 'Opender', + id: 2, + }, ], }; const order = { @@ -400,39 +406,39 @@ describe('buildRenderContext()', () => { '&~|users~|0': ['id', 'name'], '&~|users~|1': ['id', 'name'], }; - const requestGroup = { name: 'Parent', type: models.requestGroup.type, environment: obj, environmentPropertyOrder: order, }; - const rootEnvironment = { name: 'Parent', type: models.environment.type, data: obj, dataPropertyOrder: order, }; - const subEnvironment = { name: 'Sub', type: models.environment.type, data: obj, dataPropertyOrder: order, }; - const groupCtx = await renderUtils.buildRenderContext([requestGroup]); const rootCtx = await renderUtils.buildRenderContext([], rootEnvironment); const subCtx = await renderUtils.buildRenderContext([], null, subEnvironment); - const expected = { users: [ - { id: 1, name: 'Mike' }, - { id: 2, name: 'Opender' }, + { + id: 1, + name: 'Mike', + }, + { + id: 2, + name: 'Opender', + }, ], }; - expect(groupCtx).toEqual(expected); expect(rootCtx).toEqual(expected); expect(subCtx).toEqual(expected); @@ -465,9 +471,7 @@ describe('buildRenderContext()', () => { }, }, ]; - const context = await renderUtils.buildRenderContext(ancestors); - expect(context).toEqual({ parent: 'parent', test: 'parent grandparent', @@ -499,7 +503,6 @@ describe('buildRenderContext()', () => { }, }, ]; - const subEnvironment = { type: models.environment.type, data: { @@ -508,7 +511,6 @@ describe('buildRenderContext()', () => { base_url: 'https://insomnia.rest', }, }; - const rootEnvironment = { type: models.environment.type, data: { @@ -517,13 +519,11 @@ describe('buildRenderContext()', () => { base_url: 'ignore this', }, }; - const context = await renderUtils.buildRenderContext( ancestors, rootEnvironment, subEnvironment, ); - expect(context).toEqual({ base_url: 'https://insomnia.rest', url: 'https://insomnia.rest/resource', @@ -547,9 +547,7 @@ describe('buildRenderContext()', () => { '&': ['value', 'hashed', 'replaced', 'consume'], }, }; - const context = await renderUtils.buildRenderContext([], rootEnvironment); - expect(context).toEqual({ value: 'ThisIsATopSecretValue', hashed: 'f67565de946a899a534fd908e7eef872', @@ -570,9 +568,7 @@ describe('buildRenderContext()', () => { secret: 'ThisIsATopSecretValue', }, }; - const context = await renderUtils.buildRenderContext([], rootEnvironment); - expect(context).toEqual({ hash_input: '123456789012345ThisIsATopSecretValue', hash_input_expected: '123456789012345ThisIsATopSecretValue', @@ -589,19 +585,18 @@ describe('buildRenderContext()', () => { const ancestors = null; const rootEnvironment = null; const subEnvironment = null; - const context = await renderUtils.buildRenderContext( ancestors, rootEnvironment, subEnvironment, ); - expect(context).toEqual({}); }); }); describe('render()', () => { beforeEach(globalBeforeEach); + it('correctly renders simple Object', async () => { const newObj = await renderUtils.render( { @@ -614,7 +609,6 @@ describe('render()', () => { bad: 'hi', }, ); - expect(newObj).toEqual({ foo: 'bar', bar: 'bar', @@ -636,9 +630,9 @@ describe('render()', () => { arr: [1, 2, '{{ foo }}'], }, }; - - const newObj = await renderUtils.render(obj, { foo: 'bar' }); - + const newObj = await renderUtils.render(obj, { + foo: 'bar', + }); expect(newObj).toEqual({ foo: 'bar', null: null, @@ -651,7 +645,6 @@ describe('render()', () => { arr: [1, 2, 'bar'], }, }); - // Make sure original request isn't changed expect(obj.foo).toBe('{{ foo }}'); expect(obj.nested.foo).toBe('{{ foo }}'); @@ -666,7 +659,9 @@ describe('render()', () => { bar: 'bar', baz: '{{ bad }}', }, - { foo: 'bar' }, + { + foo: 'bar', + }, ); fail('Render should have failed'); } catch (err) { @@ -676,16 +671,17 @@ describe('render()', () => { it('keep on error setting', async () => { const template = '{{ foo }} {% invalid "hi" %}'; - const context = { foo: 'bar' }; - + const context = { + foo: 'bar', + }; const resultOnlyVars = await renderUtils.render( template, context, null, renderUtils.KEEP_ON_ERROR, ); - expect(resultOnlyVars).toBe('{{ foo }} {% invalid "hi" %}'); + try { await renderUtils.render(template, context, null); fail('Render should not have succeeded'); @@ -696,7 +692,11 @@ describe('render()', () => { it('outputs correct error path', async () => { const template = { - foo: [{ bar: '{% foo %}' }], + foo: [ + { + bar: '{% foo %}', + }, + ], }; try { @@ -709,7 +709,11 @@ describe('render()', () => { it('outputs correct error path when private first node', async () => { const template = { - _foo: { _bar: { baz: '{% foo %}' } }, + _foo: { + _bar: { + baz: '{% foo %}', + }, + }, }; try { @@ -733,7 +737,6 @@ describe('getRenderedGrpcRequestMessage()', () => { host: 'testb.in:9000', }, }); - const grpcRequest = await models.grpcRequest.create({ parentId: w1._id, name: 'hi {{ foo }}', diff --git a/packages/insomnia-app/app/common/__tests__/sorting.test.js b/packages/insomnia-app/app/common/__tests__/sorting.test.js deleted file mode 100644 index 92744d757f..0000000000 --- a/packages/insomnia-app/app/common/__tests__/sorting.test.js +++ /dev/null @@ -1,360 +0,0 @@ -import { - ascendingNumberSort, - descendingNumberSort, - metaSortKeySort, - sortMethodMap, -} from '../sorting'; -import { request, requestGroup, grpcRequest } from '../../models'; -import { - METHOD_DELETE, - METHOD_GET, - METHOD_HEAD, - METHOD_OPTIONS, - METHOD_PATCH, - METHOD_POST, - METHOD_PUT, - SORT_CREATED_ASC, - SORT_CREATED_DESC, - SORT_HTTP_METHOD, - SORT_NAME_ASC, - SORT_NAME_DESC, - SORT_TYPE_ASC, - SORT_TYPE_DESC, -} from '../constants'; - -describe('Sorting methods', () => { - it('sorts by name', () => { - const ascendingNameSort = sortMethodMap[SORT_NAME_ASC]; - expect(ascendingNameSort({ name: 'a' }, { name: 'b' })).toBe(-1); - expect(ascendingNameSort({ name: 'b' }, { name: 'a' })).toBe(1); - expect(ascendingNameSort({ name: 'ab' }, { name: 'abb' })).toBe(-1); - expect(ascendingNameSort({ name: 'abb' }, { name: 'ab' })).toBe(1); - expect(ascendingNameSort({ name: 'Abb' }, { name: 'bbb' })).toBe(-1); - expect(ascendingNameSort({ name: 'bbb' }, { name: 'Abb' })).toBe(1); - expect(ascendingNameSort({ name: 'abb' }, { name: 'Bbb' })).toBe(-1); - expect(ascendingNameSort({ name: 'Bbb' }, { name: 'abb' })).toBe(1); - expect(ascendingNameSort({ name: 'åbb' }, { name: 'bbb' })).toBe(-1); - expect(ascendingNameSort({ name: 'bbb' }, { name: 'åbb' })).toBe(1); - expect(ascendingNameSort({ name: 'abcdef' }, { name: 'abcdef' })).toBe(0); - - const descendingNameSort = sortMethodMap[SORT_NAME_DESC]; - expect(descendingNameSort({ name: 'a' }, { name: 'b' })).toBe(1); - expect(descendingNameSort({ name: 'b' }, { name: 'a' })).toBe(-1); - expect(descendingNameSort({ name: 'ab' }, { name: 'abb' })).toBe(1); - expect(descendingNameSort({ name: 'abb' }, { name: 'ab' })).toBe(-1); - expect(descendingNameSort({ name: 'Abb' }, { name: 'bbb' })).toBe(1); - expect(descendingNameSort({ name: 'bbb' }, { name: 'Abb' })).toBe(-1); - expect(descendingNameSort({ name: 'abb' }, { name: 'Bbb' })).toBe(1); - expect(descendingNameSort({ name: 'Bbb' }, { name: 'abb' })).toBe(-1); - expect(descendingNameSort({ name: 'åbb' }, { name: 'bbb' })).toBe(1); - expect(descendingNameSort({ name: 'bbb' }, { name: 'åbb' })).toBe(-1); - expect(descendingNameSort({ name: 'abcdef' }, { name: 'abcdef' })).toBe(0); - }); - - it('sorts by timestamp', () => { - const createdFirstSort = sortMethodMap[SORT_CREATED_ASC]; - expect(createdFirstSort({ created: 1000 }, { created: 1100 })).toBe(-1); - expect(createdFirstSort({ created: 1100 }, { created: 1000 })).toBe(1); - expect(createdFirstSort({ created: 0 }, { created: 1 })).toBe(-1); - expect(createdFirstSort({ created: 1 }, { created: 0 })).toBe(1); - expect(createdFirstSort({ created: 123456789 }, { created: 123456789 })).toBe(0); - - const createdLastSort = sortMethodMap[SORT_CREATED_DESC]; - expect(createdLastSort({ created: 1000 }, { created: 1100 })).toBe(1); - expect(createdLastSort({ created: 1100 }, { created: 1000 })).toBe(-1); - expect(createdLastSort({ created: 0 }, { created: 1 })).toBe(1); - expect(createdLastSort({ created: 1 }, { created: 0 })).toBe(-1); - expect(createdLastSort({ created: 123456789 }, { created: 123456789 })).toBe(0); - }); - - it('sorts by type', () => { - const ascendingTypeSort = sortMethodMap[SORT_TYPE_ASC]; - expect( - ascendingTypeSort( - { type: request.type, metaSortKey: 2 }, - { type: requestGroup.type, metaSortKey: 1 }, - ), - ).toBe(-1); - expect( - ascendingTypeSort( - { type: requestGroup.type, metaSortKey: 1 }, - { type: request.type, metaSortKey: 2 }, - ), - ).toBe(1); - expect( - ascendingTypeSort( - { type: request.type, metaSortKey: 2 }, - { type: grpcRequest.type, metaSortKey: 1 }, - ), - ).toBe(1); - expect( - ascendingTypeSort( - { type: grpcRequest.type, metaSortKey: 1 }, - { type: request.type, metaSortKey: 2 }, - ), - ).toBe(-1); - expect( - ascendingTypeSort( - { type: grpcRequest.type, metaSortKey: 2 }, - { type: requestGroup.type, metaSortKey: 1 }, - ), - ).toBe(-1); - expect( - ascendingTypeSort( - { type: requestGroup.type, metaSortKey: 1 }, - { type: grpcRequest.type, metaSortKey: 2 }, - ), - ).toBe(1); - expect( - ascendingTypeSort( - { type: request.type, metaSortKey: 1 }, - { type: request.type, metaSortKey: 2 }, - ), - ).toBe(-1); - expect( - ascendingTypeSort( - { type: request.type, metaSortKey: 2 }, - { type: request.type, metaSortKey: 1 }, - ), - ).toBe(1); - expect( - ascendingTypeSort( - { type: requestGroup.type, metaSortKey: 1 }, - { type: requestGroup.type, metaSortKey: 2 }, - ), - ).toBe(-1); - expect( - ascendingTypeSort( - { type: requestGroup.type, metaSortKey: 2 }, - { type: requestGroup.type, metaSortKey: 1 }, - ), - ).toBe(1); - expect( - ascendingTypeSort( - { type: grpcRequest.type, metaSortKey: 1 }, - { type: grpcRequest.type, metaSortKey: 2 }, - ), - ).toBe(-1); - expect( - ascendingTypeSort( - { type: grpcRequest.type, metaSortKey: 2 }, - { type: grpcRequest.type, metaSortKey: 1 }, - ), - ).toBe(1); - - const descendingTypeSort = sortMethodMap[SORT_TYPE_DESC]; - expect( - descendingTypeSort( - { type: request.type, metaSortKey: 2 }, - { type: requestGroup.type, metaSortKey: 1 }, - ), - ).toBe(1); - expect( - descendingTypeSort( - { type: requestGroup.type, metaSortKey: 1 }, - { type: request.type, metaSortKey: 2 }, - ), - ).toBe(-1); - expect( - descendingTypeSort( - { type: request.type, metaSortKey: 2 }, - { type: grpcRequest.type, metaSortKey: 1 }, - ), - ).toBe(1); - expect( - descendingTypeSort( - { type: grpcRequest.type, metaSortKey: 1 }, - { type: request.type, metaSortKey: 2 }, - ), - ).toBe(-1); - expect( - descendingTypeSort( - { type: grpcRequest.type, metaSortKey: 2 }, - { type: requestGroup.type, metaSortKey: 1 }, - ), - ).toBe(1); - expect( - descendingTypeSort( - { type: requestGroup.type, metaSortKey: 1 }, - { type: grpcRequest.type, metaSortKey: 2 }, - ), - ).toBe(-1); - expect( - descendingTypeSort( - { type: request.type, metaSortKey: 1 }, - { type: request.type, metaSortKey: 2 }, - ), - ).toBe(-1); - expect( - descendingTypeSort( - { type: request.type, metaSortKey: 2 }, - { type: request.type, metaSortKey: 1 }, - ), - ).toBe(1); - expect( - descendingTypeSort( - { type: requestGroup.type, metaSortKey: 1 }, - { type: requestGroup.type, metaSortKey: 2 }, - ), - ).toBe(-1); - expect( - descendingTypeSort( - { type: requestGroup.type, metaSortKey: 2 }, - { type: requestGroup.type, metaSortKey: 1 }, - ), - ).toBe(1); - expect( - descendingTypeSort( - { type: grpcRequest.type, metaSortKey: 1 }, - { type: grpcRequest.type, metaSortKey: 2 }, - ), - ).toBe(-1); - expect( - descendingTypeSort( - { type: grpcRequest.type, metaSortKey: 2 }, - { type: grpcRequest.type, metaSortKey: 1 }, - ), - ).toBe(1); - }); - - it('sorts by HTTP method', () => { - const httpMethodSort = sortMethodMap[SORT_HTTP_METHOD]; - expect(httpMethodSort({ type: request.type }, { type: requestGroup.type })).toBe(-1); - expect(httpMethodSort({ type: requestGroup.type }, { type: request.type })).toBe(1); - expect(httpMethodSort({ type: request.type }, { type: grpcRequest.type })).toBe(-1); - expect(httpMethodSort({ type: grpcRequest.type }, { type: request.type })).toBe(1); - expect(httpMethodSort({ type: requestGroup.type }, { type: grpcRequest.type })).toBe(1); - expect(httpMethodSort({ type: grpcRequest.type }, { type: requestGroup.type })).toBe(-1); - expect( - httpMethodSort( - { type: requestGroup.type, metaSortKey: 1 }, - { type: requestGroup.type, metaSortKey: 2 }, - ), - ).toBe(-1); - expect( - httpMethodSort( - { type: requestGroup.type, metaSortKey: 2 }, - { type: requestGroup.type, metaSortKey: 1 }, - ), - ).toBe(1); - expect( - httpMethodSort( - { type: grpcRequest.type, metaSortKey: 1 }, - { type: grpcRequest.type, metaSortKey: 2 }, - ), - ).toBe(-1); - expect( - httpMethodSort( - { type: grpcRequest.type, metaSortKey: 2 }, - { type: grpcRequest.type, metaSortKey: 1 }, - ), - ).toBe(1); - - expect( - httpMethodSort( - { type: request.type, method: 'CUSTOM_A' }, - { type: request.type, method: 'CUSTOM_B' }, - ), - ).toBe(-1); - expect( - httpMethodSort( - { type: request.type, method: 'CUSTOM' }, - { type: request.type, method: METHOD_GET }, - ), - ).toBe(-1); - expect( - httpMethodSort( - { type: request.type, method: METHOD_GET }, - { type: request.type, method: METHOD_POST }, - ), - ).toBe(-1); - expect( - httpMethodSort( - { type: request.type, method: METHOD_POST }, - { type: request.type, method: METHOD_PUT }, - ), - ).toBe(-1); - expect( - httpMethodSort( - { type: request.type, method: METHOD_PUT }, - { type: request.type, method: METHOD_PATCH }, - ), - ).toBe(-1); - expect( - httpMethodSort( - { type: request.type, method: METHOD_PATCH }, - { type: request.type, method: METHOD_DELETE }, - ), - ).toBe(-1); - expect( - httpMethodSort( - { type: request.type, method: METHOD_DELETE }, - { type: request.type, method: METHOD_OPTIONS }, - ), - ).toBe(-1); - expect( - httpMethodSort( - { type: request.type, method: METHOD_OPTIONS }, - { type: request.type, method: METHOD_HEAD }, - ), - ).toBe(-1); - - expect( - httpMethodSort( - { type: request.type, method: 'CUSTOM', metaSortKey: 1 }, - { type: request.type, method: 'CUSTOM', metaSortKey: 2 }, - ), - ).toBe(-1); - expect( - httpMethodSort( - { type: request.type, method: 'CUSTOM', metaSortKey: 2 }, - { type: request.type, method: 'CUSTOM', metaSortKey: 1 }, - ), - ).toBe(1); - expect( - httpMethodSort( - { type: request.type, method: METHOD_GET, metaSortKey: 1 }, - { type: request.type, method: METHOD_GET, metaSortKey: 2 }, - ), - ).toBe(-1); - expect( - httpMethodSort( - { type: request.type, method: METHOD_GET, metaSortKey: 2 }, - { type: request.type, method: METHOD_GET, metaSortKey: 1 }, - ), - ).toBe(1); - expect( - httpMethodSort( - { type: request.type, method: METHOD_DELETE, metaSortKey: 1 }, - { type: request.type, method: METHOD_DELETE, metaSortKey: 2 }, - ), - ).toBe(-1); - expect( - httpMethodSort( - { type: request.type, method: METHOD_DELETE, metaSortKey: 2 }, - { type: request.type, method: METHOD_DELETE, metaSortKey: 1 }, - ), - ).toBe(1); - }); - - it('sorts by metaSortKey', () => { - expect(metaSortKeySort({ metaSortKey: 1 }, { metaSortKey: 2 })).toBe(-1); - expect(metaSortKeySort({ metaSortKey: 2 }, { metaSortKey: 1 })).toBe(1); - expect(metaSortKeySort({ metaSortKey: -2 }, { metaSortKey: 1 })).toBe(-1); - expect(metaSortKeySort({ metaSortKey: 1 }, { metaSortKey: -2 })).toBe(1); - expect(metaSortKeySort({ metaSortKey: 1, _id: 2 }, { metaSortKey: 1, _id: 1 })).toBe(-1); - expect(metaSortKeySort({ metaSortKey: 1, _id: 1 }, { metaSortKey: 1, _id: 2 })).toBe(1); - }); - - it('sorts by number', () => { - expect(ascendingNumberSort(1, 2)).toBe(-1); - expect(ascendingNumberSort(-2, 1)).toBe(-1); - expect(ascendingNumberSort(2, 1)).toBe(1); - expect(ascendingNumberSort(1, -2)).toBe(1); - - expect(descendingNumberSort(1, 2)).toBe(1); - expect(descendingNumberSort(-2, 1)).toBe(1); - expect(descendingNumberSort(2, 1)).toBe(-1); - expect(descendingNumberSort(1, -2)).toBe(-1); - }); -}); diff --git a/packages/insomnia-app/app/common/__tests__/sorting.test.ts b/packages/insomnia-app/app/common/__tests__/sorting.test.ts new file mode 100644 index 0000000000..05b1cd4b8a --- /dev/null +++ b/packages/insomnia-app/app/common/__tests__/sorting.test.ts @@ -0,0 +1,1018 @@ +import { + ascendingNumberSort, + descendingNumberSort, + metaSortKeySort, + sortMethodMap, +} from '../sorting'; +import { request, requestGroup, grpcRequest } from '../../models'; +import { + METHOD_DELETE, + METHOD_GET, + METHOD_HEAD, + METHOD_OPTIONS, + METHOD_PATCH, + METHOD_POST, + METHOD_PUT, + SORT_CREATED_ASC, + SORT_CREATED_DESC, + SORT_HTTP_METHOD, + SORT_NAME_ASC, + SORT_NAME_DESC, + SORT_TYPE_ASC, + SORT_TYPE_DESC, +} from '../constants'; + +describe('Sorting methods', () => { + it('sorts by name', () => { + const ascendingNameSort = sortMethodMap[SORT_NAME_ASC]; + expect( + ascendingNameSort( + { + name: 'a', + }, + { + name: 'b', + }, + ), + ).toBe(-1); + expect( + ascendingNameSort( + { + name: 'b', + }, + { + name: 'a', + }, + ), + ).toBe(1); + expect( + ascendingNameSort( + { + name: 'ab', + }, + { + name: 'abb', + }, + ), + ).toBe(-1); + expect( + ascendingNameSort( + { + name: 'abb', + }, + { + name: 'ab', + }, + ), + ).toBe(1); + expect( + ascendingNameSort( + { + name: 'Abb', + }, + { + name: 'bbb', + }, + ), + ).toBe(-1); + expect( + ascendingNameSort( + { + name: 'bbb', + }, + { + name: 'Abb', + }, + ), + ).toBe(1); + expect( + ascendingNameSort( + { + name: 'abb', + }, + { + name: 'Bbb', + }, + ), + ).toBe(-1); + expect( + ascendingNameSort( + { + name: 'Bbb', + }, + { + name: 'abb', + }, + ), + ).toBe(1); + expect( + ascendingNameSort( + { + name: 'åbb', + }, + { + name: 'bbb', + }, + ), + ).toBe(-1); + expect( + ascendingNameSort( + { + name: 'bbb', + }, + { + name: 'åbb', + }, + ), + ).toBe(1); + expect( + ascendingNameSort( + { + name: 'abcdef', + }, + { + name: 'abcdef', + }, + ), + ).toBe(0); + const descendingNameSort = sortMethodMap[SORT_NAME_DESC]; + expect( + descendingNameSort( + { + name: 'a', + }, + { + name: 'b', + }, + ), + ).toBe(1); + expect( + descendingNameSort( + { + name: 'b', + }, + { + name: 'a', + }, + ), + ).toBe(-1); + expect( + descendingNameSort( + { + name: 'ab', + }, + { + name: 'abb', + }, + ), + ).toBe(1); + expect( + descendingNameSort( + { + name: 'abb', + }, + { + name: 'ab', + }, + ), + ).toBe(-1); + expect( + descendingNameSort( + { + name: 'Abb', + }, + { + name: 'bbb', + }, + ), + ).toBe(1); + expect( + descendingNameSort( + { + name: 'bbb', + }, + { + name: 'Abb', + }, + ), + ).toBe(-1); + expect( + descendingNameSort( + { + name: 'abb', + }, + { + name: 'Bbb', + }, + ), + ).toBe(1); + expect( + descendingNameSort( + { + name: 'Bbb', + }, + { + name: 'abb', + }, + ), + ).toBe(-1); + expect( + descendingNameSort( + { + name: 'åbb', + }, + { + name: 'bbb', + }, + ), + ).toBe(1); + expect( + descendingNameSort( + { + name: 'bbb', + }, + { + name: 'åbb', + }, + ), + ).toBe(-1); + expect( + descendingNameSort( + { + name: 'abcdef', + }, + { + name: 'abcdef', + }, + ), + ).toBe(0); + }); + + it('sorts by timestamp', () => { + const createdFirstSort = sortMethodMap[SORT_CREATED_ASC]; + expect( + createdFirstSort( + { + created: 1000, + }, + { + created: 1100, + }, + ), + ).toBe(-1); + expect( + createdFirstSort( + { + created: 1100, + }, + { + created: 1000, + }, + ), + ).toBe(1); + expect( + createdFirstSort( + { + created: 0, + }, + { + created: 1, + }, + ), + ).toBe(-1); + expect( + createdFirstSort( + { + created: 1, + }, + { + created: 0, + }, + ), + ).toBe(1); + expect( + createdFirstSort( + { + created: 123456789, + }, + { + created: 123456789, + }, + ), + ).toBe(0); + const createdLastSort = sortMethodMap[SORT_CREATED_DESC]; + expect( + createdLastSort( + { + created: 1000, + }, + { + created: 1100, + }, + ), + ).toBe(1); + expect( + createdLastSort( + { + created: 1100, + }, + { + created: 1000, + }, + ), + ).toBe(-1); + expect( + createdLastSort( + { + created: 0, + }, + { + created: 1, + }, + ), + ).toBe(1); + expect( + createdLastSort( + { + created: 1, + }, + { + created: 0, + }, + ), + ).toBe(-1); + expect( + createdLastSort( + { + created: 123456789, + }, + { + created: 123456789, + }, + ), + ).toBe(0); + }); + + it('sorts by type', () => { + const ascendingTypeSort = sortMethodMap[SORT_TYPE_ASC]; + expect( + ascendingTypeSort( + { + type: request.type, + metaSortKey: 2, + }, + { + type: requestGroup.type, + metaSortKey: 1, + }, + ), + ).toBe(-1); + expect( + ascendingTypeSort( + { + type: requestGroup.type, + metaSortKey: 1, + }, + { + type: request.type, + metaSortKey: 2, + }, + ), + ).toBe(1); + expect( + ascendingTypeSort( + { + type: request.type, + metaSortKey: 2, + }, + { + type: grpcRequest.type, + metaSortKey: 1, + }, + ), + ).toBe(1); + expect( + ascendingTypeSort( + { + type: grpcRequest.type, + metaSortKey: 1, + }, + { + type: request.type, + metaSortKey: 2, + }, + ), + ).toBe(-1); + expect( + ascendingTypeSort( + { + type: grpcRequest.type, + metaSortKey: 2, + }, + { + type: requestGroup.type, + metaSortKey: 1, + }, + ), + ).toBe(-1); + expect( + ascendingTypeSort( + { + type: requestGroup.type, + metaSortKey: 1, + }, + { + type: grpcRequest.type, + metaSortKey: 2, + }, + ), + ).toBe(1); + expect( + ascendingTypeSort( + { + type: request.type, + metaSortKey: 1, + }, + { + type: request.type, + metaSortKey: 2, + }, + ), + ).toBe(-1); + expect( + ascendingTypeSort( + { + type: request.type, + metaSortKey: 2, + }, + { + type: request.type, + metaSortKey: 1, + }, + ), + ).toBe(1); + expect( + ascendingTypeSort( + { + type: requestGroup.type, + metaSortKey: 1, + }, + { + type: requestGroup.type, + metaSortKey: 2, + }, + ), + ).toBe(-1); + expect( + ascendingTypeSort( + { + type: requestGroup.type, + metaSortKey: 2, + }, + { + type: requestGroup.type, + metaSortKey: 1, + }, + ), + ).toBe(1); + expect( + ascendingTypeSort( + { + type: grpcRequest.type, + metaSortKey: 1, + }, + { + type: grpcRequest.type, + metaSortKey: 2, + }, + ), + ).toBe(-1); + expect( + ascendingTypeSort( + { + type: grpcRequest.type, + metaSortKey: 2, + }, + { + type: grpcRequest.type, + metaSortKey: 1, + }, + ), + ).toBe(1); + const descendingTypeSort = sortMethodMap[SORT_TYPE_DESC]; + expect( + descendingTypeSort( + { + type: request.type, + metaSortKey: 2, + }, + { + type: requestGroup.type, + metaSortKey: 1, + }, + ), + ).toBe(1); + expect( + descendingTypeSort( + { + type: requestGroup.type, + metaSortKey: 1, + }, + { + type: request.type, + metaSortKey: 2, + }, + ), + ).toBe(-1); + expect( + descendingTypeSort( + { + type: request.type, + metaSortKey: 2, + }, + { + type: grpcRequest.type, + metaSortKey: 1, + }, + ), + ).toBe(1); + expect( + descendingTypeSort( + { + type: grpcRequest.type, + metaSortKey: 1, + }, + { + type: request.type, + metaSortKey: 2, + }, + ), + ).toBe(-1); + expect( + descendingTypeSort( + { + type: grpcRequest.type, + metaSortKey: 2, + }, + { + type: requestGroup.type, + metaSortKey: 1, + }, + ), + ).toBe(1); + expect( + descendingTypeSort( + { + type: requestGroup.type, + metaSortKey: 1, + }, + { + type: grpcRequest.type, + metaSortKey: 2, + }, + ), + ).toBe(-1); + expect( + descendingTypeSort( + { + type: request.type, + metaSortKey: 1, + }, + { + type: request.type, + metaSortKey: 2, + }, + ), + ).toBe(-1); + expect( + descendingTypeSort( + { + type: request.type, + metaSortKey: 2, + }, + { + type: request.type, + metaSortKey: 1, + }, + ), + ).toBe(1); + expect( + descendingTypeSort( + { + type: requestGroup.type, + metaSortKey: 1, + }, + { + type: requestGroup.type, + metaSortKey: 2, + }, + ), + ).toBe(-1); + expect( + descendingTypeSort( + { + type: requestGroup.type, + metaSortKey: 2, + }, + { + type: requestGroup.type, + metaSortKey: 1, + }, + ), + ).toBe(1); + expect( + descendingTypeSort( + { + type: grpcRequest.type, + metaSortKey: 1, + }, + { + type: grpcRequest.type, + metaSortKey: 2, + }, + ), + ).toBe(-1); + expect( + descendingTypeSort( + { + type: grpcRequest.type, + metaSortKey: 2, + }, + { + type: grpcRequest.type, + metaSortKey: 1, + }, + ), + ).toBe(1); + }); + + it('sorts by HTTP method', () => { + const httpMethodSort = sortMethodMap[SORT_HTTP_METHOD]; + expect( + httpMethodSort( + { + type: request.type, + }, + { + type: requestGroup.type, + }, + ), + ).toBe(-1); + expect( + httpMethodSort( + { + type: requestGroup.type, + }, + { + type: request.type, + }, + ), + ).toBe(1); + expect( + httpMethodSort( + { + type: request.type, + }, + { + type: grpcRequest.type, + }, + ), + ).toBe(-1); + expect( + httpMethodSort( + { + type: grpcRequest.type, + }, + { + type: request.type, + }, + ), + ).toBe(1); + expect( + httpMethodSort( + { + type: requestGroup.type, + }, + { + type: grpcRequest.type, + }, + ), + ).toBe(1); + expect( + httpMethodSort( + { + type: grpcRequest.type, + }, + { + type: requestGroup.type, + }, + ), + ).toBe(-1); + expect( + httpMethodSort( + { + type: requestGroup.type, + metaSortKey: 1, + }, + { + type: requestGroup.type, + metaSortKey: 2, + }, + ), + ).toBe(-1); + expect( + httpMethodSort( + { + type: requestGroup.type, + metaSortKey: 2, + }, + { + type: requestGroup.type, + metaSortKey: 1, + }, + ), + ).toBe(1); + expect( + httpMethodSort( + { + type: grpcRequest.type, + metaSortKey: 1, + }, + { + type: grpcRequest.type, + metaSortKey: 2, + }, + ), + ).toBe(-1); + expect( + httpMethodSort( + { + type: grpcRequest.type, + metaSortKey: 2, + }, + { + type: grpcRequest.type, + metaSortKey: 1, + }, + ), + ).toBe(1); + expect( + httpMethodSort( + { + type: request.type, + method: 'CUSTOM_A', + }, + { + type: request.type, + method: 'CUSTOM_B', + }, + ), + ).toBe(-1); + expect( + httpMethodSort( + { + type: request.type, + method: 'CUSTOM', + }, + { + type: request.type, + method: METHOD_GET, + }, + ), + ).toBe(-1); + expect( + httpMethodSort( + { + type: request.type, + method: METHOD_GET, + }, + { + type: request.type, + method: METHOD_POST, + }, + ), + ).toBe(-1); + expect( + httpMethodSort( + { + type: request.type, + method: METHOD_POST, + }, + { + type: request.type, + method: METHOD_PUT, + }, + ), + ).toBe(-1); + expect( + httpMethodSort( + { + type: request.type, + method: METHOD_PUT, + }, + { + type: request.type, + method: METHOD_PATCH, + }, + ), + ).toBe(-1); + expect( + httpMethodSort( + { + type: request.type, + method: METHOD_PATCH, + }, + { + type: request.type, + method: METHOD_DELETE, + }, + ), + ).toBe(-1); + expect( + httpMethodSort( + { + type: request.type, + method: METHOD_DELETE, + }, + { + type: request.type, + method: METHOD_OPTIONS, + }, + ), + ).toBe(-1); + expect( + httpMethodSort( + { + type: request.type, + method: METHOD_OPTIONS, + }, + { + type: request.type, + method: METHOD_HEAD, + }, + ), + ).toBe(-1); + expect( + httpMethodSort( + { + type: request.type, + method: 'CUSTOM', + metaSortKey: 1, + }, + { + type: request.type, + method: 'CUSTOM', + metaSortKey: 2, + }, + ), + ).toBe(-1); + expect( + httpMethodSort( + { + type: request.type, + method: 'CUSTOM', + metaSortKey: 2, + }, + { + type: request.type, + method: 'CUSTOM', + metaSortKey: 1, + }, + ), + ).toBe(1); + expect( + httpMethodSort( + { + type: request.type, + method: METHOD_GET, + metaSortKey: 1, + }, + { + type: request.type, + method: METHOD_GET, + metaSortKey: 2, + }, + ), + ).toBe(-1); + expect( + httpMethodSort( + { + type: request.type, + method: METHOD_GET, + metaSortKey: 2, + }, + { + type: request.type, + method: METHOD_GET, + metaSortKey: 1, + }, + ), + ).toBe(1); + expect( + httpMethodSort( + { + type: request.type, + method: METHOD_DELETE, + metaSortKey: 1, + }, + { + type: request.type, + method: METHOD_DELETE, + metaSortKey: 2, + }, + ), + ).toBe(-1); + expect( + httpMethodSort( + { + type: request.type, + method: METHOD_DELETE, + metaSortKey: 2, + }, + { + type: request.type, + method: METHOD_DELETE, + metaSortKey: 1, + }, + ), + ).toBe(1); + }); + + it('sorts by metaSortKey', () => { + expect( + metaSortKeySort( + { + metaSortKey: 1, + }, + { + metaSortKey: 2, + }, + ), + ).toBe(-1); + expect( + metaSortKeySort( + { + metaSortKey: 2, + }, + { + metaSortKey: 1, + }, + ), + ).toBe(1); + expect( + metaSortKeySort( + { + metaSortKey: -2, + }, + { + metaSortKey: 1, + }, + ), + ).toBe(-1); + expect( + metaSortKeySort( + { + metaSortKey: 1, + }, + { + metaSortKey: -2, + }, + ), + ).toBe(1); + expect( + metaSortKeySort( + { + metaSortKey: 1, + _id: 2, + }, + { + metaSortKey: 1, + _id: 1, + }, + ), + ).toBe(-1); + expect( + metaSortKeySort( + { + metaSortKey: 1, + _id: 1, + }, + { + metaSortKey: 1, + _id: 2, + }, + ), + ).toBe(1); + }); + + it('sorts by number', () => { + expect(ascendingNumberSort(1, 2)).toBe(-1); + expect(ascendingNumberSort(-2, 1)).toBe(-1); + expect(ascendingNumberSort(2, 1)).toBe(1); + expect(ascendingNumberSort(1, -2)).toBe(1); + expect(descendingNumberSort(1, 2)).toBe(1); + expect(descendingNumberSort(-2, 1)).toBe(1); + expect(descendingNumberSort(2, 1)).toBe(-1); + expect(descendingNumberSort(1, -2)).toBe(-1); + }); +}); diff --git a/packages/insomnia-app/app/common/__tests__/strings.test.js b/packages/insomnia-app/app/common/__tests__/strings.test.ts similarity index 98% rename from packages/insomnia-app/app/common/__tests__/strings.test.js rename to packages/insomnia-app/app/common/__tests__/strings.test.ts index ab27aa9daa..318de788e1 100644 --- a/packages/insomnia-app/app/common/__tests__/strings.test.js +++ b/packages/insomnia-app/app/common/__tests__/strings.test.ts @@ -1,4 +1,3 @@ -// @flow import * as models from '../../models'; import { strings } from '../strings'; import { getWorkspaceLabel } from '../get-workspace-label'; diff --git a/packages/insomnia-app/app/common/analytics.js b/packages/insomnia-app/app/common/analytics.ts similarity index 72% rename from packages/insomnia-app/app/common/analytics.js rename to packages/insomnia-app/app/common/analytics.ts index e015c958d1..66cc0e22e5 100644 --- a/packages/insomnia-app/app/common/analytics.js +++ b/packages/insomnia-app/app/common/analytics.ts @@ -1,8 +1,7 @@ -// @flow import { buildQueryStringFromParams, joinUrlAndQueryString } from 'insomnia-url'; import * as electron from 'electron'; import * as models from '../models/index'; -import * as db from '../common/database'; +import { database as db } from '../common/database'; import * as uuid from 'uuid'; import { getAppId, @@ -15,13 +14,12 @@ import { isDevelopment, } from './constants'; import type { RequestParameter } from '../models/request'; -import { getScreenResolution, getUserLanguage, getViewportSize } from './misc'; +import { getScreenResolution, getUserLanguage, getViewportSize } from './electron-helpers'; import Analytics from 'analytics-node'; import { getAccountId } from '../account/session'; const DIMENSION_PLATFORM = 1; const DIMENSION_VERSION = 2; - const KEY_TRACKING_ID = 'tid'; const KEY_VERSION = 'v'; const KEY_CLIENT_ID = 'cid'; @@ -42,12 +40,15 @@ const KEY_ANONYMIZE_IP = 'aip'; const KEY_APPLICATION_NAME = 'an'; const KEY_APPLICATION_ID = 'aid'; const KEY_APPLICATION_VERSION = 'av'; - const KEY_CUSTOM_DIMENSION_PREFIX = 'cd'; - let _currentLocationPath = '/'; -export function trackEvent(category: string, action: string, label: ?string, value: ?string) { +export function trackEvent( + category: string, + action: string, + label?: string | null, + value?: string | null, +) { process.nextTick(async () => { await _trackEvent(true, category, action, label, value); }); @@ -56,8 +57,8 @@ export function trackEvent(category: string, action: string, label: ?string, val export function trackNonInteractiveEvent( category: string, action: string, - label: ?string, - value: ?string, + label?: string | null, + value?: string | null, ) { process.nextTick(async () => { await _trackEvent(false, category, action, label, value, false); @@ -79,8 +80,8 @@ export function trackNonInteractiveEvent( export function trackNonInteractiveEventQueueable( category: string, action: string, - label: ?string, - value: ?string, + label?: string | null, + value?: string | null, ) { process.nextTick(async () => { await _trackEvent(false, category, action, label, value, true); @@ -93,25 +94,27 @@ export function trackPageView(path: string) { }); } -export async function getDeviceId(): Promise { +export async function getDeviceId() { const settings = await models.settings.getOrCreate(); - let { deviceId } = settings; + if (!deviceId) { // Migrate old GA ID into settings model if needed const oldId = (window && window.localStorage.getItem('gaClientId')) || null; deviceId = oldId || uuid.v4(); - - await models.settings.update(settings, { deviceId }); + await models.settings.update(settings, { + deviceId, + }); } return deviceId; } -let segmentClient = null; +let segmentClient: Analytics | null = null; -export async function trackSegmentEvent(event: String, properties?: Object) { +export async function trackSegmentEvent(event: string, properties?: Record) { const settings = await models.settings.getOrCreate(); + if (!settings.enableAnalytics) { return; } @@ -119,19 +122,20 @@ export async function trackSegmentEvent(event: String, properties?: Object) { try { if (!segmentClient) { segmentClient = new Analytics(getSegmentWriteKey(), { + // @ts-expect-error -- TSCONVERSION axiosConfig: { // This is needed to ensure that we use the NodeJS adapter in the render process - ...(global?.require && { adapter: global.require('axios/lib/adapters/http') }), + ...(global?.require && { + adapter: global.require('axios/lib/adapters/http'), + }), }, }); } const anonymousId = await getDeviceId(); - // TODO: This currently always returns an empty string in the main process // This is due to the session data being stored in localStorage const userId = getAccountId(); - segmentClient.track({ anonymousId, userId, @@ -156,14 +160,16 @@ export async function trackSegmentEvent(event: String, properties?: Object) { // ~~~~~~~~~~~~~~~~~ // // Private Functions // // ~~~~~~~~~~~~~~~~~ // - -function _getOsName(): string { +function _getOsName() { const platform = getAppPlatform(); + switch (platform) { case 'darwin': return 'mac'; + case 'win32': return 'windows'; + default: return platform; } @@ -174,69 +180,133 @@ export async function _trackEvent( interactive: boolean, category: string, action: string, - label: ?string, - value: ?string, - queuable: ?boolean, + label?: string | null, + value?: string | null, + queuable?: boolean | null, ) { const prefix = interactive ? '[ga] Event' : '[ga] Non-interactive'; console.log(prefix, [category, action, label, value].filter(Boolean).join(', ')); - const params = [ - { name: KEY_HIT_TYPE, value: 'event' }, - { name: KEY_EVENT_CATEGORY, value: category }, - { name: KEY_EVENT_ACTION, value: action }, + { + name: KEY_HIT_TYPE, + value: 'event', + }, + { + name: KEY_EVENT_CATEGORY, + value: category, + }, + { + name: KEY_EVENT_ACTION, + value: action, + }, ]; - - !interactive && params.push({ name: KEY_NON_INTERACTION, value: '1' }); - label && params.push({ name: KEY_EVENT_LABEL, value: label }); - value && params.push({ name: KEY_EVENT_VALUE, value: value }); - + !interactive && + params.push({ + name: KEY_NON_INTERACTION, + value: '1', + }); + label && + params.push({ + name: KEY_EVENT_LABEL, + value: label, + }); + value && + params.push({ + name: KEY_EVENT_VALUE, + value: value, + }); + // @ts-expect-error -- TSCONVERSION appears to be a genuine error await _sendToGoogle(params, !!queuable); } export async function _trackPageView(location: string) { _currentLocationPath = location; console.log('[ga] Page', _currentLocationPath); - - const params = [{ name: KEY_HIT_TYPE, value: 'pageview' }]; - + const params = [ + { + name: KEY_HIT_TYPE, + value: 'pageview', + }, + ]; + // @ts-expect-error -- TSCONVERSION appears to be a genuine error await _sendToGoogle(params, false); } async function _getDefaultParams(): Promise> { const deviceId = await getDeviceId(); - // Prepping user agent string prior to sending to GA due to Electron base UA not being GA friendly. const ua = String(window?.navigator?.userAgent) .replace(new RegExp(`${getAppId()}\\/\\d+\\.\\d+\\.\\d+ `), '') .replace(/Electron\/\d+\.\d+\.\d+ /, ''); - const params = [ - { name: KEY_VERSION, value: '1' }, - { name: KEY_TRACKING_ID, value: getGoogleAnalyticsId() }, - { name: KEY_CLIENT_ID, value: deviceId }, - { name: KEY_USER_AGENT, value: ua }, - { name: KEY_LOCATION, value: getGoogleAnalyticsLocation() + _currentLocationPath }, - { name: KEY_SCREEN_RESOLUTION, value: getScreenResolution() }, - { name: KEY_USER_LANGUAGE, value: getUserLanguage() }, - { name: KEY_TITLE, value: `${getAppId()}:${getAppVersion()}` }, - { name: KEY_CUSTOM_DIMENSION_PREFIX + DIMENSION_PLATFORM, value: getAppPlatform() }, - { name: KEY_CUSTOM_DIMENSION_PREFIX + DIMENSION_VERSION, value: getAppVersion() }, - { name: KEY_ANONYMIZE_IP, value: '1' }, - { name: KEY_APPLICATION_NAME, value: getAppName() }, - { name: KEY_APPLICATION_ID, value: getAppId() }, - { name: KEY_APPLICATION_VERSION, value: getAppVersion() }, + { + name: KEY_VERSION, + value: '1', + }, + { + name: KEY_TRACKING_ID, + value: getGoogleAnalyticsId(), + }, + { + name: KEY_CLIENT_ID, + value: deviceId, + }, + { + name: KEY_USER_AGENT, + value: ua, + }, + { + name: KEY_LOCATION, + value: getGoogleAnalyticsLocation() + _currentLocationPath, + }, + { + name: KEY_SCREEN_RESOLUTION, + value: getScreenResolution(), + }, + { + name: KEY_USER_LANGUAGE, + value: getUserLanguage(), + }, + { + name: KEY_TITLE, + value: `${getAppId()}:${getAppVersion()}`, + }, + { + name: KEY_CUSTOM_DIMENSION_PREFIX + DIMENSION_PLATFORM, + value: getAppPlatform(), + }, + { + name: KEY_CUSTOM_DIMENSION_PREFIX + DIMENSION_VERSION, + value: getAppVersion(), + }, + { + name: KEY_ANONYMIZE_IP, + value: '1', + }, + { + name: KEY_APPLICATION_NAME, + value: getAppName(), + }, + { + name: KEY_APPLICATION_ID, + value: getAppId(), + }, + { + name: KEY_APPLICATION_VERSION, + value: getAppVersion(), + }, ]; - const viewport = getViewportSize(); - viewport && params.push({ name: KEY_VIEWPORT_SIZE, value: viewport }); - + viewport && + params.push({ + name: KEY_VIEWPORT_SIZE, + value: viewport, + }); global.document && params.push({ name: KEY_DOCUMENT_ENCODING, value: global.document.inputEncoding, }); - return params; } @@ -245,6 +315,7 @@ async function _getDefaultParams(): Promise> { db.onChange(async changes => { for (const change of changes) { const [event, doc] = change; + if (doc.type === models.settings.type && event === 'update') { if (doc.enableAnalytics) { await _flushQueuedEvents(); @@ -253,38 +324,40 @@ db.onChange(async changes => { } }); -async function _sendToGoogle(params: Array, queueable: boolean) { +async function _sendToGoogle(params: RequestParameter, queueable: boolean) { const settings = await models.settings.getOrCreate(); + if (!settings.enableAnalytics) { if (queueable) { console.log('[ga] Queued event', params); + _queuedEvents.push(params); } + return; } const baseParams = await _getDefaultParams(); + // @ts-expect-error -- TSCONVERSION appears to be a genuine error const allParams = [...baseParams, ...params]; const qs = buildQueryStringFromParams(allParams); const baseUrl = isDevelopment() ? 'https://www.google-analytics.com/debug/collect' : 'https://www.google-analytics.com/collect'; const url = joinUrlAndQueryString(baseUrl, qs); - const net = (electron.remote || electron).net; const request = net.request(url); - request.once('error', err => { console.warn('[ga] Network error', err); }); - request.once('response', response => { const { statusCode } = response; + if (statusCode < 200 && statusCode >= 300) { console.warn('[ga] Bad status code ' + statusCode); } - const chunks = []; + const chunks: Array = []; const [contentType] = response.headers['content-type'] || []; if (contentType !== 'application/json') { @@ -294,9 +367,11 @@ async function _sendToGoogle(params: Array, queueable: boolean response.on('end', () => { const jsonStr = Buffer.concat(chunks).toString('utf8'); + try { const data = JSON.parse(jsonStr); const { hitParsingResult } = data; + if (hitParsingResult.valid) { return; } @@ -310,12 +385,10 @@ async function _sendToGoogle(params: Array, queueable: boolean console.warn('[ga] Failed to parse response', err); } }); - response.on('data', chunk => { chunks.push(chunk); }); }); - request.end(); } @@ -325,13 +398,11 @@ async function _sendToGoogle(params: Array, queueable: boolean * @returns {Promise} * @private */ -let _queuedEvents = []; +let _queuedEvents: Array = []; async function _flushQueuedEvents() { console.log(`[ga] Flushing ${_queuedEvents.length} queued events`); - const tmp = [..._queuedEvents]; - // Clear queue before we even start sending to prevent races _queuedEvents = []; diff --git a/packages/insomnia-app/app/common/api-specs.js b/packages/insomnia-app/app/common/api-specs.ts similarity index 79% rename from packages/insomnia-app/app/common/api-specs.js rename to packages/insomnia-app/app/common/api-specs.ts index 03c20660bf..97efc58ad3 100644 --- a/packages/insomnia-app/app/common/api-specs.js +++ b/packages/insomnia-app/app/common/api-specs.ts @@ -1,16 +1,16 @@ -// @flow - import YAML from 'yaml'; +export interface ParsedApiSpec { + contents: Record | null; + rawContents: string; + format: 'openapi' | 'swagger' | null; + formatVersion: string | null; +} + export function parseApiSpec( rawDocument: string, -): { - contents: Object | null, - rawContents: string, - format: 'openapi' | 'swagger' | null, - formatVersion: string | null, -} { - const result = { +) { + const result: ParsedApiSpec = { contents: null, rawContents: rawDocument, format: null, diff --git a/packages/insomnia-app/app/common/constants.js b/packages/insomnia-app/app/common/constants.ts similarity index 87% rename from packages/insomnia-app/app/common/constants.js rename to packages/insomnia-app/app/common/constants.ts index 90007fb271..a5621a5a5b 100644 --- a/packages/insomnia-app/app/common/constants.js +++ b/packages/insomnia-app/app/common/constants.ts @@ -1,96 +1,33 @@ -// @flow -import { appConfig } from '../../config'; -import * as electron from 'electron'; +import appConfig from '../../config/config.json'; import path from 'path'; -import mkdirp from 'mkdirp'; -import { getDataDirectory } from './misc'; +import { ValueOf } from 'type-fest'; +import { getDataDirectory } from './electron-helpers'; // App Stuff +export const getAppVersion = () => appConfig.version; +export const getAppLongName = () => appConfig.longName; +export const getAppName = () => appConfig.productName; +export const getAppDefaultTheme = () => appConfig.theme; +export const getAppDefaultLightTheme = () => appConfig.lightTheme; +export const getAppDefaultDarkTheme = () => appConfig.darkTheme; +export const getAppSynopsis = () => appConfig.synopsis; +export const getAppId = () => appConfig.appId; +export const getGoogleAnalyticsId = () => appConfig.gaId; +export const getGoogleAnalyticsLocation = () => appConfig.gaLocation; +export const getAppPlatform = () => process.platform; +export const isMac = () => getAppPlatform() === 'darwin'; +export const isLinux = () => getAppPlatform() === 'linux'; +export const isWindows = () => getAppPlatform() === 'win32'; +export const getAppEnvironment = () => process.env.INSOMNIA_ENV || 'production'; +export const isDevelopment = () => getAppEnvironment() === 'development'; +export const getSegmentWriteKey = () => appConfig.segmentWriteKeys[isDevelopment() ? 'development' : 'production']; +export const getAppReleaseDate = () => new Date(process.env.RELEASE_DATE ?? '').toLocaleDateString(); -export function getAppVersion() { - return appConfig().version; -} - -export function getAppLongName() { - return appConfig().longName; -} - -export function getAppName() { - return appConfig().productName; -} - -export function getAppDefaultTheme() { - return appConfig().theme; -} - -export function getAppDefaultLightTheme() { - return appConfig().lightTheme; -} - -export function getAppDefaultDarkTheme() { - return appConfig().darkTheme; -} - -export function getAppSynopsis() { - return appConfig().synopsis; -} - -export function getAppId() { - return appConfig().appId; -} - -export function getGoogleAnalyticsId() { - return appConfig().gaId; -} - -export function getGoogleAnalyticsLocation() { - return appConfig().gaLocation; -} - -export function getSegmentWriteKey() { - if (isDevelopment()) { - return appConfig().segmentWriteKeys.development; - } - - return appConfig().segmentWriteKeys.production; -} - -export function getAppPlatform() { - return process.platform; -} - -export function getAppEnvironment() { - return process.env.INSOMNIA_ENV || 'production'; -} - -export function getAppReleaseDate() { - return new Date(process.env.RELEASE_DATE).toLocaleDateString(); -} - -export function getBrowserUserAgent() { - const ua = encodeURIComponent( - String(window.navigator.userAgent) - .replace(new RegExp(`${getAppId()}\\/\\d+\\.\\d+\\.\\d+ `), '') - .replace(/Electron\/\d+\.\d+\.\d+ /, ''), - ).replace('%2C', ','); - return ua; -} - -export function getTempDir() { - // NOTE: Using a fairly unique name here because "insomnia" is a common word - const { app } = electron.remote || electron; - const dir = path.join(app.getPath('temp'), `insomnia_${getAppVersion()}`); - mkdirp.sync(dir); - return dir; -} - -export function isMac() { - return getAppPlatform() === 'darwin'; -} - -export function isLinux() { - return getAppPlatform() === 'linux'; -} +export const getBrowserUserAgent = () => encodeURIComponent( + String(window.navigator.userAgent) + .replace(new RegExp(`${getAppId()}\\/\\d+\\.\\d+\\.\\d+ `), '') + .replace(/Electron\/\d+\.\d+\.\d+ /, ''), +).replace('%2C', ','); export function updatesSupported() { // Updates are not supported on Linux @@ -106,21 +43,8 @@ export function updatesSupported() { return true; } -export function isWindows() { - return getAppPlatform() === 'win32'; -} - -export function isDevelopment() { - return getAppEnvironment() === 'development'; -} - -export function getClientString() { - return `${getAppEnvironment()}::${getAppPlatform()}::${getAppVersion()}`; -} - -export function changelogUrl(): string { - return appConfig().changelogUrl; -} +export const getClientString = () => `${getAppEnvironment()}::${getAppPlatform()}::${getAppVersion()}`; +export const changelogUrl = () => appConfig.changelogUrl; // Global Stuff export const DB_PERSIST_INTERVAL = 1000 * 60 * 30; // Compact every once in a while @@ -140,33 +64,33 @@ export const AUTOBIND_CFG = { ], }; -// Available editor key maps +// Available editor key map export const EDITOR_KEY_MAP_DEFAULT = 'default'; export const EDITOR_KEY_MAP_EMACS = 'emacs'; export const EDITOR_KEY_MAP_SUBLIME = 'sublime'; export const EDITOR_KEY_MAP_VIM = 'vim'; -// Hotkeys +// Hotkey export const MNEMONIC_SYM = isMac() ? '' : '&'; export const CTRL_SYM = isMac() ? '⌃' : 'Ctrl'; export const ALT_SYM = isMac() ? '⌥' : 'Alt'; export const SHIFT_SYM = isMac() ? '⇧' : 'Shift'; export const META_SYM = isMac() ? '⌘' : 'Super'; -// Updates +// Update export const UPDATE_CHANNEL_STABLE = 'stable'; export const UPDATE_CHANNEL_BETA = 'beta'; export const UPDATE_URL_MAC = 'https://updates.insomnia.rest/builds/check/mac'; export const UPDATE_URL_WINDOWS = 'https://updates.insomnia.rest/updates/win'; -// API +// AP export const API_BASE_URL = 'https://api.insomnia.rest'; -// PLUGINS +// PLUGIN export const PLUGIN_HUB_BASE = 'https://insomnia.rest/plugins'; export const NPM_PACKAGE_BASE = 'https://www.npmjs.com/package'; -// UI Stuff +// UI Stuf export const MAX_SIDEBAR_REMS = 45; export const MIN_SIDEBAR_REMS = 0.75; export const COLLAPSE_SIDEBAR_REMS = 3; @@ -201,12 +125,13 @@ export const ACTIVITY_MIGRATION: GlobalActivity = 'migration'; export const ACTIVITY_ANALYTICS: GlobalActivity = 'analytics'; export const DEPRECATED_ACTIVITY_INSOMNIA = 'insomnia'; -export const isWorkspaceActivity = (activity: GlobalActivity): boolean => { +export const isWorkspaceActivity = (activity: string): activity is GlobalActivity => { switch (activity) { case ACTIVITY_SPEC: case ACTIVITY_DEBUG: case ACTIVITY_UNIT_TEST: return true; + case ACTIVITY_HOME: case ACTIVITY_ONBOARDING: case ACTIVITY_MIGRATION: @@ -216,7 +141,7 @@ export const isWorkspaceActivity = (activity: GlobalActivity): boolean => { } }; -export const isValidActivity = (activity: GlobalActivity): boolean => { +export const isValidActivity = (activity: string): activity is GlobalActivity => { switch (activity) { case ACTIVITY_SPEC: case ACTIVITY_DEBUG: @@ -226,6 +151,7 @@ export const isValidActivity = (activity: GlobalActivity): boolean => { case ACTIVITY_MIGRATION: case ACTIVITY_ANALYTICS: return true; + default: return false; } @@ -256,13 +182,11 @@ export const METHOD_GRPC = 'GRPC'; export const PREVIEW_MODE_FRIENDLY = 'friendly'; export const PREVIEW_MODE_SOURCE = 'source'; export const PREVIEW_MODE_RAW = 'raw'; - const previewModeMap = { [PREVIEW_MODE_FRIENDLY]: ['Preview', 'Visual Preview'], [PREVIEW_MODE_SOURCE]: ['Source', 'Source Code'], [PREVIEW_MODE_RAW]: ['Raw', 'Raw Data'], }; - export const PREVIEW_MODES = Object.keys(previewModeMap); // Content Types @@ -275,7 +199,6 @@ export const CONTENT_TYPE_FORM_DATA = 'multipart/form-data'; export const CONTENT_TYPE_FILE = 'application/octet-stream'; export const CONTENT_TYPE_GRAPHQL = 'application/graphql'; export const CONTENT_TYPE_OTHER = ''; - const contentTypesMap = { [CONTENT_TYPE_EDN]: ['EDN', 'EDN'], [CONTENT_TYPE_FILE]: ['File', 'Binary File'], @@ -300,7 +223,6 @@ export const AUTH_HAWK = 'hawk'; export const AUTH_AWS_IAM = 'iam'; export const AUTH_NETRC = 'netrc'; export const AUTH_ASAP = 'asap'; - export const HAWK_ALGORITHM_SHA256 = 'sha256'; export const HAWK_ALGORITHM_SHA1 = 'sha1'; @@ -315,9 +237,8 @@ export const HttpVersions = { V2_0: 'V2_0', v3: 'v3', default: 'default', -}; - -export type HttpVersion = $Keys; +} as const; +export type HttpVersion = ValueOf; const authTypesMap = { [AUTH_BASIC]: ['Basic', 'Basic Auth'], @@ -330,15 +251,15 @@ const authTypesMap = { [AUTH_AWS_IAM]: ['AWS', 'AWS IAM v4'], [AUTH_ASAP]: ['ASAP', 'Atlassian ASAP'], [AUTH_NETRC]: ['Netrc', 'Netrc File'], -}; +} as const; // Sort Orders export type SortOrder = | 'name-asc' | 'name-desc' - | 'created-first' - | 'created-last' - | 'method' + | 'created-asc' + | 'created-desc' + | 'http-method' | 'type-desc' | 'type-asc'; export const SORT_NAME_ASC: SortOrder = 'name-asc'; @@ -357,8 +278,7 @@ export const SORT_ORDERS = [ SORT_TYPE_DESC, SORT_TYPE_ASC, ]; - -export const sortOrderName: { [SortOrder]: string } = { +export const sortOrderName: Record = { [SORT_NAME_ASC]: 'Name Ascending', [SORT_NAME_DESC]: 'Name Descending', [SORT_CREATED_ASC]: 'Oldest First', @@ -376,7 +296,7 @@ export function getPreviewModeName(previewMode, useLong = false) { } } -export function getContentTypeName(contentType, useLong = false) { +export function getContentTypeName(contentType?: string | null, useLong = false) { if (typeof contentType !== 'string') { return ''; } @@ -396,7 +316,7 @@ export function getAuthTypeName(authType, useLong = false) { } } -export function getContentTypeFromHeaders(headers, defaultValue = null) { +export function getContentTypeFromHeaders(headers, defaultValue: string | null = null) { if (!Array.isArray(headers)) { return null; } @@ -410,14 +330,10 @@ export const RESPONSE_CODE_DESCRIPTIONS = { // Special [STATUS_CODE_PLUGIN_ERROR]: 'An Insomnia plugin threw an error which prevented the request from sending', - // 100s - 100: 'This interim response indicates that everything so far is OK and that the client should continue with the request or ignore it if it is already finished.', 101: 'This code is sent in response to an Upgrade: request header by the client and indicates the protocol the server is switching to. It was introduced to allow migration to an incompatible protocol version, and it is not in common use.', - // 200s - 200: 'The request has succeeded.', 201: 'The request has succeeded and a new resource has been created as a result. This is typically the response sent after POST requests, or some PUT requests.', 202: 'The request has been received but not yet acted upon. It is non-committal, meaning that there is no way in HTTP to later send an asynchronous response indicating the outcome of processing the request. It is intended for cases where another process or server handles the request, or for batch processing.', @@ -428,9 +344,7 @@ export const RESPONSE_CODE_DESCRIPTIONS = { 207: 'A Multi-Status response conveys information about multiple resources in situations where multiple status codes might be appropriate.', 208: 'Used inside a DAV: propstat response element to avoid enumerating the internal members of multiple bindings to the same collection repeatedly.', 226: 'The server has fulfilled a GET request for the resource, and the response is a representation of the result of one or more instance-manipulations applied to the current instance.', - // 300s - 300: 'The request has more than one possible responses. User-agent or user should choose one of them. There is no standardized way to choose one of the responses.', 301: 'This response code means that URI of requested resource has been changed. Probably, new URI would be given in the response.', 302: 'This response code means that URI of requested resource has been changed temporarily. New changes in the URI might be made in the future. Therefore, this same URI should be used by the client in future requests.', @@ -440,9 +354,7 @@ export const RESPONSE_CODE_DESCRIPTIONS = { 306: 'This response code is no longer used and is just reserved currently. It was used in a previous version of the HTTP 1.1 specification.', 307: 'Server sent this response to directing client to get requested resource to another URI with same method that used prior request. This has the same semantic than the 302 Found HTTP response code, with the exception that the user agent must not change the HTTP method used: if a POST was used in the first request, a POST must be used in the second request.', 308: 'This means that the resource is now permanently located at another URI, specified by the Location: HTTP Response header. This has the same semantics as the 301 Moved Permanently HTTP response code, with the exception that the user agent must not change the HTTP method used: if a POST was used in the first request, a POST must be used in the second request.', - // 400s - 400: 'This response means that the server could not understand the request due to invalid syntax.', 401: 'Authentication is needed to get the requested response. This is similar to 403, but is different in that authentication is possible.', 402: 'This response code is reserved for future use. Initial aim for creating this code was using it for digital payment systems, but it is not used currently.', @@ -471,9 +383,7 @@ export const RESPONSE_CODE_DESCRIPTIONS = { 429: 'The user has sent too many requests in a given amount of time ("rate limiting").', 431: 'The server is unwilling to process the request because its header fields are too large. The request MAY be resubmitted after reducing the size of the request header fields.', 451: 'The user requests an illegal resource, such as a web page censored by a government.', - // 500s - 500: "The server has encountered a situation it doesn't know how to handle.", 501: 'The request method is not supported by the server and cannot be handled. The only methods that servers are required to support (and therefore that must not return this code) are GET and HEAD.', 502: 'This error response means that the server, while working as a gateway to get a response needed to handle the request, got an invalid response.', @@ -490,14 +400,10 @@ export const RESPONSE_CODE_DESCRIPTIONS = { export const RESPONSE_CODE_REASONS = { // Special [STATUS_CODE_PLUGIN_ERROR]: 'Plugin Error', - // 100s - 100: 'Continue', 101: 'Switching Protocols', - // 200s - 200: 'OK', 201: 'Created', 202: 'Accepted', @@ -508,9 +414,7 @@ export const RESPONSE_CODE_REASONS = { 207: 'Multi-Status', 208: 'Already Reported', 226: 'IM Used', - // 300s - 300: 'Multiple Choices', 301: 'Moved Permanently', 302: 'Found', @@ -520,9 +424,7 @@ export const RESPONSE_CODE_REASONS = { 306: 'Switch Proxy', 307: 'Temporary Redirect', 308: 'Permanent Redirect', - // 400s - 400: 'Bad Request', 401: 'Unauthorized', 402: 'Payment Required', @@ -552,9 +454,7 @@ export const RESPONSE_CODE_REASONS = { 429: 'Too Many Requests', 431: 'Request Header Fields Too Large', 451: 'Unavailable For Legal Reasons', - // 500s - 500: 'Internal Server Error', 501: 'Not Implemented', 502: 'Bad Gateway', diff --git a/packages/insomnia-app/app/common/database.js b/packages/insomnia-app/app/common/database.js deleted file mode 100644 index 292167ebaf..0000000000 --- a/packages/insomnia-app/app/common/database.js +++ /dev/null @@ -1,812 +0,0 @@ -// @flow -import type { BaseModel } from '../models/index'; -import * as models from '../models/index'; -import electron from 'electron'; -import NeDB from 'nedb'; -import fsPath from 'path'; -import { DB_PERSIST_INTERVAL } from './constants'; -import * as uuid from 'uuid'; -import { generateId, getDataDirectory } from './misc'; -import { mustGetModel } from '../models'; -import type { Workspace } from '../models/workspace'; - -export const CHANGE_INSERT = 'insert'; -export const CHANGE_UPDATE = 'update'; -export const CHANGE_REMOVE = 'remove'; - -const database = {}; -const db = ({ - _empty: true, -}: Object); - -// ~~~~~~~ // -// HELPERS // -// ~~~~~~~ // - -function allTypes() { - return Object.keys(db); -} - -function getDBFilePath(modelType) { - // NOTE: Do not EVER change this. EVER! - return fsPath.join(getDataDirectory(), `insomnia.${modelType}.db`); -} - -export async function initClient() { - electron.ipcRenderer.on('db.changes', async (e, changes) => { - for (const fn of changeListeners) { - await fn(changes); - } - }); - console.log('[db] Initialized DB client'); -} - -export async function init( - types: Array, - config: Object = {}, - forceReset: boolean = false, - consoleLog: () => void = console.log, -) { - if (forceReset) { - changeListeners = []; - for (const attr of Object.keys(db)) { - if (attr === '_empty') { - continue; - } - - delete db[attr]; - } - } - - // Fill in the defaults - for (const modelType of types) { - if (db[modelType]) { - consoleLog(`[db] Already initialized DB.${modelType}`); - continue; - } - - const filePath = getDBFilePath(modelType); - const collection = new NeDB( - Object.assign( - { - autoload: true, - filename: filePath, - corruptAlertThreshold: 0.9, - }, - config, - ), - ); - - collection.persistence.setAutocompactionInterval(DB_PERSIST_INTERVAL); - - db[modelType] = collection; - } - - delete db._empty; - - electron.ipcMain.on('db.fn', async (e, fnName, replyChannel, ...args) => { - try { - const result = await database[fnName](...args); - e.sender.send(replyChannel, null, result); - } catch (err) { - e.sender.send(replyChannel, { message: err.message, stack: err.stack }); - } - }); - - // NOTE: Only repair the DB if we're not running in memory. Repairing here causes tests to - // hang indefinitely for some reason. - // TODO: Figure out why this makes tests hang - if (!config.inMemoryOnly) { - await _repairDatabase(); - } - - if (!config.inMemoryOnly) { - consoleLog(`[db] Initialized DB at ${getDBFilePath('$TYPE')}`); - } - - // This isn't the best place for this but w/e - // Listen for response deletions and delete corresponding response body files - onChange(async changes => { - for (const [type, doc] of changes) { - const m: Object | null = models.getModel(doc.type); - - if (!m) { - continue; - } - - if (type === CHANGE_REMOVE && typeof m.hookRemove === 'function') { - try { - await m.hookRemove(doc, consoleLog); - } catch (err) { - consoleLog(`[db] Delete hook failed for ${type} ${doc._id}: ${err.message}`); - } - } - - if (type === CHANGE_INSERT && typeof m.hookInsert === 'function') { - try { - await m.hookInsert(doc, consoleLog); - } catch (err) { - consoleLog(`[db] Insert hook failed for ${type} ${doc._id}: ${err.message}`); - } - } - - if (type === CHANGE_UPDATE && typeof m.hookUpdate === 'function') { - try { - await m.hookUpdate(doc, consoleLog); - } catch (err) { - consoleLog(`[db] Update hook failed for ${type} ${doc._id}: ${err.message}`); - } - } - } - }); - - for (const model of models.all()) { - if (typeof model.hookDatabaseInit === 'function') { - await model.hookDatabaseInit(consoleLog); - } - } -} - -// ~~~~~~~~~~~~~~~~ // -// Change Listeners // -// ~~~~~~~~~~~~~~~~ // - -let bufferingChanges = false; -let bufferChangesId = 1; -let changeBuffer = []; -let changeListeners = []; - -export function onChange(callback: Function): void { - changeListeners.push(callback); -} - -export function offChange(callback: Function): void { - changeListeners = changeListeners.filter(l => l !== callback); -} - -/** buffers database changes and returns a buffer id */ -export const bufferChanges = (database.bufferChanges = async function( - millis: number = 1000, -): Promise { - if (db._empty) return _send('bufferChanges', ...arguments); - - bufferingChanges = true; - setTimeout(database.flushChanges, millis); - - return ++bufferChangesId; -}); - -/** buffers database changes and returns a buffer id */ -export const bufferChangesIndefinitely = (database.bufferChangesIndefinitely = async function(): Promise { - if (db._empty) return _send('bufferChangesIndefinitely', ...arguments); - - bufferingChanges = true; - - return ++bufferChangesId; -}); - -export const flushChangesAsync = (database.flushChangesAsync = async function( - fake: boolean = false, -) { - process.nextTick(async () => { - await flushChanges(0, fake); - }); -}); - -export const flushChanges = (database.flushChanges = async function( - id: number = 0, - fake: boolean = false, -) { - if (db._empty) return _send('flushChanges', ...arguments); - - // Only flush if ID is 0 or the current flush ID is the same as passed - if (id !== 0 && bufferChangesId !== id) { - return; - } - - bufferingChanges = false; - const changes = [...changeBuffer]; - changeBuffer = []; - - if (changes.length === 0) { - // No work to do - return; - } - - if (fake) { - console.log(`[db] Dropped ${changes.length} changes.`); - return; - } - - // Notify local listeners too - for (const fn of changeListeners) { - await fn(changes); - } - - // Notify remote listeners - const windows = electron.BrowserWindow.getAllWindows(); - for (const window of windows) { - window.webContents.send('db.changes', changes); - } -}); - -async function notifyOfChange(event: string, doc: BaseModel, fromSync: boolean): Promise { - changeBuffer.push([event, doc, fromSync]); - - // Flush right away if we're not buffering - if (!bufferingChanges) { - await database.flushChanges(); - } -} - -// ~~~~~~~ // -// Helpers // -// ~~~~~~~ // - -export const getMostRecentlyModified = (database.getMostRecentlyModified = async function( - type: string, - query: Object = {}, -): Promise { - if (db._empty) return _send('getMostRecentlyModified', ...arguments); - - const docs = await database.findMostRecentlyModified(type, query, 1); - return docs.length ? docs[0] : null; -}); - -export const findMostRecentlyModified = (database.findMostRecentlyModified = async function( - type: string, - query: Object = {}, - limit: number | null = null, -): Promise> { - if (db._empty) return _send('findMostRecentlyModified', ...arguments); - - return new Promise(resolve => { - db[type] - .find(query) - .sort({ modified: -1 }) - .limit(limit) - .exec(async (err, rawDocs) => { - if (err) { - console.warn('[db] Failed to find docs', err); - resolve([]); - return; - } - - const docs = []; - for (const rawDoc of rawDocs) { - docs.push(await models.initModel(type, rawDoc)); - } - - resolve(docs); - }); - }); -}); - -export const find = (database.find = async function( - type: string, - query: Object = {}, - sort: Object = { created: 1 }, -): Promise> { - if (db._empty) return _send('find', ...arguments); - - return new Promise((resolve, reject) => { - db[type] - .find(query) - .sort(sort) - .exec(async (err, rawDocs) => { - if (err) { - return reject(err); - } - - const docs = []; - for (const rawDoc of rawDocs) { - docs.push(await models.initModel(type, rawDoc)); - } - - resolve(docs); - }); - }); -}); - -export const all = (database.all = async function(type: string): Promise> { - if (db._empty) return _send('all', ...arguments); - - return database.find(type); -}); - -export const getWhere = (database.getWhere = async function( - type: string, - query: Object, -): Promise { - if (db._empty) return _send('getWhere', ...arguments); - - const docs = await database.find(type, query); - return docs.length ? docs[0] : null; -}); - -export const get = (database.get = async function( - type: string, - id: string, -): Promise { - if (db._empty) return _send('get', ...arguments); - - // Short circuit IDs used to represent nothing - if (!id || id === 'n/a') { - return null; - } else { - return database.getWhere(type, { _id: id }); - } -}); - -export const count = (database.count = async function( - type: string, - query: Object = {}, -): Promise { - if (db._empty) return _send('count', ...arguments); - - return new Promise((resolve, reject) => { - db[type].count(query, (err, count) => { - if (err) { - return reject(err); - } - - resolve(count); - }); - }); -}); - -export const upsert = (database.upsert = async function( - doc: BaseModel, - fromSync: boolean = false, -): Promise { - if (db._empty) return _send('upsert', ...arguments); - - const existingDoc = await database.get(doc.type, doc._id); - if (existingDoc) { - return database.update(doc, fromSync); - } else { - return database.insert(doc, fromSync); - } -}); - -export const insert = (database.insert = async function( - doc: T, - fromSync: boolean = false, - initializeModel: boolean = true, -): Promise { - if (db._empty) return _send('insert', ...arguments); - - return new Promise(async (resolve, reject) => { - let docWithDefaults; - try { - if (initializeModel) { - docWithDefaults = await models.initModel(doc.type, doc); - } else { - docWithDefaults = doc; - } - } catch (err) { - return reject(err); - } - - db[doc.type].insert(docWithDefaults, (err, newDoc) => { - if (err) { - return reject(err); - } - - resolve(newDoc); - - // NOTE: This needs to be after we resolve - notifyOfChange(CHANGE_INSERT, newDoc, fromSync); - }); - }); -}); - -export const update = (database.update = async function( - doc: T, - fromSync: boolean = false, -): Promise { - if (db._empty) return _send('update', ...arguments); - - return new Promise(async (resolve, reject) => { - let docWithDefaults; - try { - docWithDefaults = await models.initModel(doc.type, doc); - } catch (err) { - return reject(err); - } - - db[doc.type].update({ _id: docWithDefaults._id }, docWithDefaults, err => { - if (err) { - return reject(err); - } - - resolve(docWithDefaults); - - // NOTE: This needs to be after we resolve - notifyOfChange(CHANGE_UPDATE, docWithDefaults, fromSync); - }); - }); -}); - -export const remove = (database.remove = async function( - doc: T, - fromSync: boolean = false, -): Promise { - if (db._empty) return _send('remove', ...arguments); - - const flushId = await database.bufferChanges(); - - const docs = await database.withDescendants(doc); - const docIds = docs.map(d => d._id); - const types = [...new Set(docs.map(d => d.type))]; - - // Don't really need to wait for this to be over; - types.map(t => db[t].remove({ _id: { $in: docIds } }, { multi: true })); - - docs.map(d => notifyOfChange(CHANGE_REMOVE, d, fromSync)); - - await database.flushChanges(flushId); -}); - -/** Removes entries without removing their children */ -export const unsafeRemove = (database.unsafeRemove = async function( - doc: T, - fromSync: boolean = false, -): Promise { - if (db._empty) return _send('unsafeRemove', ...arguments); - - db[doc.type].remove({ _id: doc._id }); - notifyOfChange(CHANGE_REMOVE, doc, fromSync); -}); - -export const removeWhere = (database.removeWhere = async function( - type: string, - query: Object, -): Promise { - if (db._empty) return _send('removeWhere', ...arguments); - - const flushId = await database.bufferChanges(); - - for (const doc of await database.find(type, query)) { - const docs = await database.withDescendants(doc); - const docIds = docs.map(d => d._id); - const types = [...new Set(docs.map(d => d.type))]; - - // Don't really need to wait for this to be over; - types.map(t => db[t].remove({ _id: { $in: docIds } }, { multi: true })); - - docs.map(d => notifyOfChange(CHANGE_REMOVE, d, false)); - } - - await database.flushChanges(flushId); -}); - -export const batchModifyDocs = (database.batchModifyDocs = async function(operations: { - upsert: Array, - remove: Array, -}): Promise { - if (db._empty) return _send('batchModifyDocs', ...arguments); - - const flushId = await bufferChanges(); - - const promisesUpserted = []; - const promisesDeleted = []; - for (const doc: BaseModel of operations.upsert) { - promisesUpserted.push(upsert(doc, true)); - } - - for (const doc: BaseModel of operations.remove) { - promisesDeleted.push(unsafeRemove(doc, true)); - } - - // Perform from least to most dangerous - await Promise.all(promisesUpserted); - await Promise.all(promisesDeleted); - - await flushChanges(flushId); -}); - -// ~~~~~~~~~~~~~~~~~~~ // -// DEFAULT MODEL STUFF // -// ~~~~~~~~~~~~~~~~~~~ // - -export async function docUpdate( - originalDoc: T, - ...patches: Array -): Promise { - // No need to re-initialize the model during update; originalDoc will be in a valid state by virtue of loading - const doc = await models.initModel( - originalDoc.type, - originalDoc, - - // NOTE: This is before `patch` because we want `patch.modified` to win if it has it - { modified: Date.now() }, - - ...patches, - ); - - return database.update(doc); -} - -export async function docCreate(type: string, ...patches: Array): Promise { - const doc = await models.initModel( - type, - ...patches, - - // Fields that the user can't touch - { type: type }, - ); - - return database.insert(doc); -} - -// ~~~~~~~ // -// GENERAL // -// ~~~~~~~ // - -export const withDescendants = (database.withDescendants = async function( - doc: BaseModel | null, - stopType: string | null = null, -): Promise> { - if (db._empty) return _send('withDescendants', ...arguments); - - let docsToReturn = doc ? [doc] : []; - - async function next(docs: Array): Promise> { - let foundDocs = []; - - for (const d of docs) { - if (stopType && d && d.type === stopType) { - continue; - } - - const promises = []; - for (const type of allTypes()) { - // If the doc is null, we want to search for parentId === null - const parentId = d ? d._id : null; - promises.push(database.find(type, { parentId })); - } - - for (const more of await Promise.all(promises)) { - foundDocs = [...foundDocs, ...more]; - } - } - - if (foundDocs.length === 0) { - // Didn't find anything. We're done - return docsToReturn; - } - - // Continue searching for children - docsToReturn = [...docsToReturn, ...foundDocs]; - return next(foundDocs); - } - - return next([doc]); -}); - -export const withAncestors = (database.withAncestors = async function( - doc: BaseModel | null, - types: Array = allTypes(), -): Promise> { - if (db._empty) return _send('withAncestors', ...arguments); - - if (!doc) { - return []; - } - - let docsToReturn = doc ? [doc] : []; - - async function next(docs: Array): Promise> { - const foundDocs = []; - for (const d: BaseModel of docs) { - for (const type of types) { - // If the doc is null, we want to search for parentId === null - const another = await database.get(type, d.parentId); - another && foundDocs.push(another); - } - } - - if (foundDocs.length === 0) { - // Didn't find anything. We're done - return docsToReturn; - } - - // Continue searching for children - docsToReturn = [...docsToReturn, ...foundDocs]; - return next(foundDocs); - } - - return next([doc]); -}); - -export const duplicate = (database.duplicate = async function( - originalDoc: T, - patch: Object = {}, -): Promise { - if (db._empty) return _send('duplicate', ...arguments); - - const flushId = await database.bufferChanges(); - - async function next(docToCopy: T, patch: Object): Promise { - const model = mustGetModel(docToCopy.type); - - const overrides = { - _id: generateId(model.prefix), - modified: Date.now(), - created: Date.now(), - type: docToCopy.type, // Ensure this is not overwritten by the patch - }; - - // 1. Copy the doc - const newDoc = Object.assign({}, docToCopy, patch, overrides); - - // Don't initialize the model during insert, and simply duplicate - const createdDoc = await database.insert(newDoc, false, false); - - // 2. Get all the children - for (const type of allTypes()) { - // Note: We never want to duplicate a response - if (!models.canDuplicate(type)) { - continue; - } - - const parentId = docToCopy._id; - const children = await database.find(type, { parentId }); - for (const doc of children) { - await next(doc, { parentId: createdDoc._id }); - } - } - - return createdDoc; - } - - const createdDoc = await next(originalDoc, patch); - - await database.flushChanges(flushId); - - return createdDoc; -}); - -// ~~~~~~~ // -// Helpers // -// ~~~~~~~ // - -async function _send(fnName: string, ...args: Array): Promise { - return new Promise((resolve, reject) => { - const replyChannel = `db.fn.reply:${uuid.v4()}`; - electron.ipcRenderer.send('db.fn', fnName, replyChannel, ...args); - electron.ipcRenderer.once(replyChannel, (e, err, result) => { - if (err) { - reject(err); - } else { - resolve(result); - } - }); - }); -} - -/** - * Run various database repair scripts - */ -export async function _repairDatabase() { - console.log('[fix] Running database repairs'); - for (const workspace of await find(models.workspace.type)) { - await _repairBaseEnvironments(workspace); - await _fixMultipleCookieJars(workspace); - await _applyApiSpecName(workspace); - } - - for (const gitRepository of await find(models.gitRepository.type)) { - await _fixOldGitURIs(gitRepository); - } -} - -/** - * This function ensures that apiSpec exists for each workspace - * If the filename on the apiSpec is not set or is the default initialized name - * It will apply the workspace name to it - */ -async function _applyApiSpecName(workspace: Workspace) { - const apiSpec = await models.apiSpec.getByParentId(workspace._id); - - if (!apiSpec.fileName || apiSpec.fileName === models.apiSpec.init().fileName) { - await models.apiSpec.update(apiSpec, { fileName: workspace.name }); - } -} - -/** - * This function repairs workspaces that have multiple base environments. Since a workspace - * can only have one, this function walks over all base environments, merges the data, and - * moves all children as well. - */ -async function _repairBaseEnvironments(workspace) { - const baseEnvironments = await find(models.environment.type, { - parentId: workspace._id, - }); - - // Nothing to do here - if (baseEnvironments.length <= 1) { - return; - } - - const chosenBase = baseEnvironments[0]; - for (const baseEnvironment of baseEnvironments) { - if (baseEnvironment._id === chosenBase._id) { - continue; - } - - chosenBase.data = Object.assign(baseEnvironment.data, chosenBase.data); - const subEnvironments = await find(models.environment.type, { - parentId: baseEnvironment._id, - }); - - for (const subEnvironment of subEnvironments) { - await docUpdate(subEnvironment, { parentId: chosenBase._id }); - } - - // Remove unnecessary base env - await remove(baseEnvironment); - } - - // Update remaining base env - await update(chosenBase); - - console.log(`[fix] Merged ${baseEnvironments.length} base environments under ${workspace.name}`); -} - -/** - * This function repairs workspaces that have multiple cookie jars. Since a workspace - * can only have one, this function walks over all jars and merges them and their cookies - * together. - */ -async function _fixMultipleCookieJars(workspace) { - const cookieJars = await find(models.cookieJar.type, { - parentId: workspace._id, - }); - - // Nothing to do here - if (cookieJars.length <= 1) { - return; - } - - const chosenJar = cookieJars[0]; - for (const cookieJar of cookieJars) { - if (cookieJar._id === chosenJar._id) { - continue; - } - - for (const cookie of cookieJar.cookies) { - if (chosenJar.cookies.find(c => c.id === cookie.id)) { - continue; - } - - chosenJar.cookies.push(cookie); - } - - // Remove unnecessary jar - await remove(cookieJar); - } - - // Update remaining jar - await update(chosenJar); - - console.log(`[fix] Merged ${cookieJars.length} cookie jars under ${workspace.name}`); -} - -// Append .git to old git URIs to mimic previous isomorphic-git behaviour -async function _fixOldGitURIs(doc: GitRepository) { - if (!doc.uriNeedsMigration) { - return; - } - - if (!doc.uri.endsWith('.git')) { - doc.uri += '.git'; - } - - doc.uriNeedsMigration = false; - - await update(doc); - - console.log(`[fix] Fixed git URI for ${doc._id}`); -} diff --git a/packages/insomnia-app/app/common/database.ts b/packages/insomnia-app/app/common/database.ts new file mode 100644 index 0000000000..a206de7e5f --- /dev/null +++ b/packages/insomnia-app/app/common/database.ts @@ -0,0 +1,830 @@ +/* eslint-disable prefer-rest-params -- don't want to change ...arguments usage for these sensitive functions without more testing */ +import type { BaseModel } from '../models/index'; +import * as models from '../models/index'; +import electron from 'electron'; +import NeDB from 'nedb'; +import fsPath from 'path'; +import { DB_PERSIST_INTERVAL } from './constants'; +import * as uuid from 'uuid'; +import { generateId } from './misc'; +import { getDataDirectory } from './electron-helpers'; +import { mustGetModel } from '../models'; +import type { Workspace } from '../models/workspace'; +import { GitRepository } from '../models/git-repository'; +import { CookieJar } from '../models/cookie-jar'; +import { Environment } from '../models/environment'; + +export interface Query { + _id?: string | SpecificQuery; + parentId?: string | null; + plugin?: string; + key?: string; + environmentId?: string | null; + protoFileId?: string; +} + +type Sort = Record; + +interface Operation { + upsert?: Array; + remove?: Array; +} + +export interface SpecificQuery { + $gt?: number; + $in?: Array; + $nin?: Array; +} + +export type ModelQuery = Partial>; + +export const database = { + all: async function(type: string) { + if (db._empty) return _send>('all', ...arguments); + return database.find(type); + }, + + batchModifyDocs: async function(operation: Operation) { + if (db._empty) return _send('batchModifyDocs', ...arguments); + const flushId = await database.bufferChanges(); + const promisesUpserted: Array> = []; + const promisesDeleted: Array> = []; + + // @ts-expect-error -- TSCONVERSION upsert operations are optional + for (const doc of operation.upsert) { + promisesUpserted.push(database.upsert(doc, true)); + } + + // @ts-expect-error -- TSCONVERSION remove operations are optional + for (const doc of operation.remove) { + promisesDeleted.push(database.unsafeRemove(doc, true)); + } + + // Perform from least to most dangerous + await Promise.all(promisesUpserted); + await Promise.all(promisesDeleted); + + await database.flushChanges(flushId); + }, + + /** buffers database changes and returns a buffer id */ + bufferChanges: async function(millis = 1000) { + if (db._empty) return _send('bufferChanges', ...arguments); + bufferingChanges = true; + setTimeout(database.flushChanges, millis); + return ++bufferChangesId; + }, + + /** buffers database changes and returns a buffer id */ + bufferChangesIndefinitely: async function() { + if (db._empty) return _send('bufferChangesIndefinitely', ...arguments); + bufferingChanges = true; + return ++bufferChangesId; + }, + + CHANGE_INSERT: 'insert', + + CHANGE_UPDATE: 'update', + + CHANGE_REMOVE: 'remove', + + count: async function(type: string, query: Query = {}) { + if (db._empty) return _send('count', ...arguments); + return new Promise((resolve, reject) => { + (db[type] as NeDB).count(query, (err, count) => { + if (err) { + return reject(err); + } + + resolve(count); + }); + }); + }, + + docCreate: async (type: string, ...patches: Array>) => { + const doc = await models.initModel( + type, + ...patches, + // Fields that the user can't touch + { + type: type, + }, + ); + return database.insert(doc); + }, + + docUpdate: async (originalDoc: T, ...patches: Array>) => { + // No need to re-initialize the model during update; originalDoc will be in a valid state by virtue of loading + const doc = await models.initModel( + originalDoc.type, + originalDoc, + + // NOTE: This is before `patches` because we want `patch.modified` to win if it has it + { + modified: Date.now(), + }, + ...patches, + ); + return database.update(doc); + }, + + duplicate: async function(originalDoc: T, patch: Patch = {}) { + if (db._empty) return _send('duplicate', ...arguments); + const flushId = await database.bufferChanges(); + + async function next(docToCopy: T, patch: Patch) { + const model = mustGetModel(docToCopy.type); + const overrides = { + _id: generateId(model.prefix), + modified: Date.now(), + created: Date.now(), + type: docToCopy.type, // Ensure this is not overwritten by the patch + }; + + // 1. Copy the doc + const newDoc = Object.assign({}, docToCopy, patch, overrides); + + // Don't initialize the model during insert, and simply duplicate + const createdDoc = await database.insert(newDoc, false, false); + + // 2. Get all the children + for (const type of allTypes()) { + // Note: We never want to duplicate a response + if (!models.canDuplicate(type)) { + continue; + } + + const parentId = docToCopy._id; + const children = await database.find(type, { parentId }); + + for (const doc of children) { + await next(doc, { parentId: createdDoc._id }); + } + } + + return createdDoc; + } + + const createdDoc = await next(originalDoc, patch); + await database.flushChanges(flushId); + return createdDoc; + }, + + find: async function( + type: string, + query: Query | string = {}, + sort: Sort = { created: 1 }, + ) { + if (db._empty) return _send>('find', ...arguments); + return new Promise>((resolve, reject) => { + (db[type] as NeDB) + .find(query) + .sort(sort) + .exec(async (err, rawDocs) => { + if (err) { + reject(err); + return; + } + + const docs: Array = []; + + for (const rawDoc of rawDocs) { + docs.push(await models.initModel(type, rawDoc)); + } + + resolve(docs); + }); + }); + }, + + findMostRecentlyModified: async function( + type: string, + query: Query = {}, + limit: number | null = null, + ) { + if (db._empty) return _send>('findMostRecentlyModified', ...arguments); + return new Promise>(resolve => { + (db[type] as NeDB) + .find(query) + .sort({ + modified: -1, + }) + // @ts-expect-error -- TSCONVERSION limit shouldn't be applied if it's null, or default to something that means no-limit + .limit(limit) + .exec(async (err, rawDocs) => { + if (err) { + console.warn('[db] Failed to find docs', err); + resolve([]); + return; + } + + const docs: Array = []; + + for (const rawDoc of rawDocs) { + docs.push(await models.initModel(type, rawDoc)); + } + + resolve(docs); + }); + }); + }, + + flushChanges: async function(id = 0, fake = false) { + if (db._empty) return _send('flushChanges', ...arguments); + + // Only flush if ID is 0 or the current flush ID is the same as passed + if (id !== 0 && bufferChangesId !== id) { + return; + } + + bufferingChanges = false; + const changes = [...changeBuffer]; + changeBuffer = []; + + if (changes.length === 0) { + // No work to do + return; + } + + if (fake) { + console.log(`[db] Dropped ${changes.length} changes.`); + return; + } + + // Notify local listeners too + for (const fn of changeListeners) { + await fn(changes); + } + + // Notify remote listeners + const windows = electron.BrowserWindow.getAllWindows(); + + for (const window of windows) { + window.webContents.send('db.changes', changes); + } + }, + + flushChangesAsync: async (fake = false) => { + process.nextTick(async () => { + await database.flushChanges(0, fake); + }); + }, + + get: async function(type: string, id?: string) { + if (db._empty) return _send('get', ...arguments); + + // Short circuit IDs used to represent nothing + if (!id || id === 'n/a') { + return null; + } else { + return database.getWhere(type, { _id: id }); + } + }, + + getMostRecentlyModified: async function(type: string, query: Query = {}) { + if (db._empty) return _send('getMostRecentlyModified', ...arguments); + const docs = await database.findMostRecentlyModified(type, query, 1); + return docs.length ? docs[0] : null; + }, + + getWhere: async function(type: string, query: ModelQuery | Query) { + if (db._empty) return _send('getWhere', ...arguments); + // @ts-expect-error -- TSCONVERSION type narrowing needed + const docs = await database.find(type, query); + return docs.length ? docs[0] : null; + }, + + init: async ( + types: Array, + config: NeDB.DataStoreOptions = {}, + forceReset = false, + consoleLog: typeof console.log = console.log, + ) => { + if (forceReset) { + changeListeners = []; + + for (const attr of Object.keys(db)) { + if (attr === '_empty') { + continue; + } + + delete db[attr]; + } + } + + // Fill in the defaults + for (const modelType of types) { + if (db[modelType]) { + consoleLog(`[db] Already initialized DB.${modelType}`); + continue; + } + + const filePath = getDBFilePath(modelType); + const collection = new NeDB( + Object.assign( + { + autoload: true, + filename: filePath, + corruptAlertThreshold: 0.9, + }, + config, + ), + ); + collection.persistence.setAutocompactionInterval(DB_PERSIST_INTERVAL); + db[modelType] = collection; + } + + delete db._empty; + electron.ipcMain.on('db.fn', async (e, fnName, replyChannel, ...args) => { + try { + const result = await database[fnName](...args); + e.sender.send(replyChannel, null, result); + } catch (err) { + e.sender.send(replyChannel, { + message: err.message, + stack: err.stack, + }); + } + }); + + // NOTE: Only repair the DB if we're not running in memory. Repairing here causes tests to hang indefinitely for some reason. + // TODO: Figure out why this makes tests hang + if (!config.inMemoryOnly) { + await _repairDatabase(); + consoleLog(`[db] Initialized DB at ${getDBFilePath('$TYPE')}`); + } + + // This isn't the best place for this but w/e + // Listen for response deletions and delete corresponding response body files + database.onChange(async changes => { + for (const [type, doc] of changes) { + // TODO(TSCONVERSION) what's returned here is the entire model implementation, not just a model + // The type definition will be a little confusing + const m: Record | null = models.getModel(doc.type); + + if (!m) { + continue; + } + + if (type === database.CHANGE_REMOVE && typeof m.hookRemove === 'function') { + try { + await m.hookRemove(doc, consoleLog); + } catch (err) { + consoleLog(`[db] Delete hook failed for ${type} ${doc._id}: ${err.message}`); + } + } + + if (type === database.CHANGE_INSERT && typeof m.hookInsert === 'function') { + try { + await m.hookInsert(doc, consoleLog); + } catch (err) { + consoleLog(`[db] Insert hook failed for ${type} ${doc._id}: ${err.message}`); + } + } + + if (type === database.CHANGE_UPDATE && typeof m.hookUpdate === 'function') { + try { + await m.hookUpdate(doc, consoleLog); + } catch (err) { + consoleLog(`[db] Update hook failed for ${type} ${doc._id}: ${err.message}`); + } + } + } + }); + + for (const model of models.all()) { + // @ts-expect-error -- TSCONVERSION optional type on response + if (typeof model.hookDatabaseInit === 'function') { + // @ts-expect-error -- TSCONVERSION optional type on response + await model.hookDatabaseInit?.(consoleLog); + } + } + }, + + initClient: async () => { + electron.ipcRenderer.on('db.changes', async (_e, changes) => { + for (const fn of changeListeners) { + await fn(changes); + } + }); + console.log('[db] Initialized DB client'); + }, + + insert: async function(doc: T, fromSync = false, initializeModel = true) { + if (db._empty) return _send('insert', ...arguments); + return new Promise(async (resolve, reject) => { + let docWithDefaults: T | null = null; + + try { + if (initializeModel) { + docWithDefaults = await models.initModel(doc.type, doc); + } else { + docWithDefaults = doc; + } + } catch (err) { + return reject(err); + } + + (db[doc.type] as NeDB).insert(docWithDefaults, (err, newDoc: T) => { + if (err) { + return reject(err); + } + + resolve(newDoc); + // NOTE: This needs to be after we resolve + notifyOfChange(database.CHANGE_INSERT, newDoc, fromSync); + }); + }); + }, + + onChange: (callback: ChangeListener) => { + changeListeners.push(callback); + }, + + offChange: (callback: ChangeListener) => { + changeListeners = changeListeners.filter(l => l !== callback); + }, + + remove: async function(doc: T, fromSync = false) { + if (db._empty) return _send('remove', ...arguments); + + const flushId = await database.bufferChanges(); + + const docs = await database.withDescendants(doc); + const docIds = docs.map(d => d._id); + const types = [...new Set(docs.map(d => d.type))]; + + // Don't really need to wait for this to be over; + types.map(t => + db[t].remove( + { + _id: { + $in: docIds, + }, + }, + { + multi: true, + }, + ), + ); + + docs.map(d => notifyOfChange(database.CHANGE_REMOVE, d, fromSync)); + await database.flushChanges(flushId); + }, + + removeWhere: async function(type: string, query: Query) { + if (db._empty) return _send('removeWhere', ...arguments); + const flushId = await database.bufferChanges(); + + for (const doc of await database.find(type, query)) { + const docs = await database.withDescendants(doc); + const docIds = docs.map(d => d._id); + const types = [...new Set(docs.map(d => d.type))]; + + // Don't really need to wait for this to be over; + types.map(t => + db[t].remove( + { + _id: { + $in: docIds, + }, + }, + { + multi: true, + }, + ), + ); + docs.map(d => notifyOfChange(database.CHANGE_REMOVE, d, false)); + } + + await database.flushChanges(flushId); + }, + + /** Removes entries without removing their children */ + unsafeRemove: async function(doc: T, fromSync = false) { + if (db._empty) return _send('unsafeRemove', ...arguments); + + (db[doc.type] as NeDB).remove({ _id: doc._id }); + notifyOfChange(database.CHANGE_REMOVE, doc, fromSync); + }, + + update: async function(doc: T, fromSync = false) { + if (db._empty) return _send('update', ...arguments); + + return new Promise(async (resolve, reject) => { + let docWithDefaults: T; + + try { + docWithDefaults = await models.initModel(doc.type, doc); + } catch (err) { + return reject(err); + } + + (db[doc.type] as NeDB).update( + { _id: docWithDefaults._id }, + docWithDefaults, + // TODO(TSCONVERSION) see comment below, upsert can happen automatically as part of the update + // @ts-expect-error -- TSCONVERSION expects 4 args but only sent 3. Need to validate what UpdateOptions should be. + err => { + if (err) { + return reject(err); + } + + resolve(docWithDefaults); + // NOTE: This needs to be after we resolve + notifyOfChange(database.CHANGE_UPDATE, docWithDefaults, fromSync); + }, + ); + }); + }, + + // TODO(TSCONVERSION) the update method above can now take an upsert property + upsert: async function(doc: T, fromSync = false) { + if (db._empty) return _send('upsert', ...arguments); + const existingDoc = await database.get(doc.type, doc._id); + + if (existingDoc) { + return database.update(doc, fromSync); + } else { + return database.insert(doc, fromSync); + } + }, + + withAncestors: async function(doc: T | null, types: Array = allTypes()) { + if (db._empty) return _send>('withAncestors', ...arguments); + + if (!doc) { + return []; + } + + let docsToReturn: Array = doc ? [doc] : []; + + async function next(docs: Array) { + const foundDocs: Array = []; + + for (const d of docs) { + for (const type of types) { + // If the doc is null, we want to search for parentId === null + const another = await database.get(type, d.parentId); + another && foundDocs.push(another); + } + } + + if (foundDocs.length === 0) { + // Didn't find anything. We're done + return docsToReturn; + } + + // Continue searching for children + docsToReturn = [ + ...docsToReturn, + ...foundDocs, + ]; + return next(foundDocs); + } + + return next([doc]); + }, + + withDescendants: async function(doc: T | null, stopType: string | null = null) { + if (db._empty) return _send>('withDescendants', ...arguments); + let docsToReturn = doc ? [doc] : []; + + async function next(docs: Array): Promise> { + let foundDocs: Array = []; + + for (const doc of docs) { + if (stopType && doc && doc.type === stopType) { + continue; + } + + const promises: Array>> = []; + + for (const type of allTypes()) { + // If the doc is null, we want to search for parentId === null + const parentId = doc ? doc._id : null; + const promise = database.find(type, { parentId }); + promises.push(promise); + } + + for (const more of await Promise.all(promises)) { + foundDocs = [ + ...foundDocs, + ...more, + ]; + } + } + + if (foundDocs.length === 0) { + // Didn't find anything. We're done + return docsToReturn; + } + + // Continue searching for children + docsToReturn = [...docsToReturn, ...foundDocs]; + return next(foundDocs); + } + + return next([doc]); + }, +}; + +interface DB { + [index: string]: NeDB; +} + +// @ts-expect-error -- TSCONVERSION _empty doesn't match the index signature, use something other than _empty in future +const db: DB = { + _empty: true, +} as DB; + +// ~~~~~~~ // +// HELPERS // +// ~~~~~~~ // +const allTypes = () => Object.keys(db); + +function getDBFilePath(modelType: string) { + // NOTE: Do not EVER change this. EVER! + return fsPath.join(getDataDirectory(), `insomnia.${modelType}.db`); +} + +// ~~~~~~~~~~~~~~~~ // +// Change Listeners // +// ~~~~~~~~~~~~~~~~ // +let bufferingChanges = false; +let bufferChangesId = 1; + +type ChangeBufferEvent = [ + event: string, + doc: BaseModel, + fromSync: boolean +]; + +let changeBuffer: Array = []; + +type ChangeListener = Function; + +let changeListeners: Array = []; + +async function notifyOfChange(event: string, doc: T, fromSync: boolean) { + changeBuffer.push([event, doc, fromSync]); + + // Flush right away if we're not buffering + if (!bufferingChanges) { + await database.flushChanges(); + } +} + +// ~~~~~~~~~~~~~~~~~~~ // +// DEFAULT MODEL STUFF // +// ~~~~~~~~~~~~~~~~~~~ // + +type Patch = Partial; + +// ~~~~~~~ // +// Helpers // +// ~~~~~~~ // +async function _send(fnName: string, ...args: Array) { + return new Promise((resolve, reject) => { + const replyChannel = `db.fn.reply:${uuid.v4()}`; + electron.ipcRenderer.send('db.fn', fnName, replyChannel, ...args); + electron.ipcRenderer.once(replyChannel, (_e, err, result: T) => { + if (err) { + reject(err); + } else { + resolve(result); + } + }); + }); +} + +/** + * Run various database repair scripts + */ +export async function _repairDatabase() { + console.log('[fix] Running database repairs'); + + for (const workspace of await database.find(models.workspace.type)) { + await _repairBaseEnvironments(workspace); + await _fixMultipleCookieJars(workspace); + await _applyApiSpecName(workspace); + } + + for (const gitRepository of await database.find(models.gitRepository.type)) { + await _fixOldGitURIs(gitRepository); + } +} + +/** + * This function ensures that apiSpec exists for each workspace + * If the filename on the apiSpec is not set or is the default initialized name + * It will apply the workspace name to it + */ +async function _applyApiSpecName(workspace: Workspace) { + const apiSpec = await models.apiSpec.getByParentId(workspace._id); + if (apiSpec === null) { + return; + } + + if (!apiSpec.fileName || apiSpec.fileName === models.apiSpec.init().fileName) { + await models.apiSpec.update(apiSpec, { + fileName: workspace.name, + }); + } +} + +/** + * This function repairs workspaces that have multiple base environments. Since a workspace + * can only have one, this function walks over all base environments, merges the data, and + * moves all children as well. + */ +async function _repairBaseEnvironments(workspace: Workspace) { + const baseEnvironments = await database.find(models.environment.type, { + parentId: workspace._id, + }); + + // Nothing to do here + if (baseEnvironments.length <= 1) { + return; + } + + const chosenBase = baseEnvironments[0]; + + for (const baseEnvironment of baseEnvironments) { + if (baseEnvironment._id === chosenBase._id) { + continue; + } + + chosenBase.data = Object.assign(baseEnvironment.data, chosenBase.data); + const subEnvironments = await database.find(models.environment.type, { + parentId: baseEnvironment._id, + }); + + for (const subEnvironment of subEnvironments) { + await database.docUpdate(subEnvironment, { + parentId: chosenBase._id, + }); + } + + // Remove unnecessary base env + await database.remove(baseEnvironment); + } + + // Update remaining base env + await database.update(chosenBase); + console.log(`[fix] Merged ${baseEnvironments.length} base environments under ${workspace.name}`); +} + +/** + * This function repairs workspaces that have multiple cookie jars. Since a workspace + * can only have one, this function walks over all jars and merges them and their cookies + * together. + */ +async function _fixMultipleCookieJars(workspace: Workspace) { + const cookieJars = await database.find(models.cookieJar.type, { + parentId: workspace._id, + }); + + // Nothing to do here + if (cookieJars.length <= 1) { + return; + } + + const chosenJar = cookieJars[0]; + + for (const cookieJar of cookieJars) { + if (cookieJar._id === chosenJar._id) { + continue; + } + + for (const cookie of cookieJar.cookies) { + if (chosenJar.cookies.find(c => c.id === cookie.id)) { + continue; + } + + chosenJar.cookies.push(cookie); + } + + // Remove unnecessary jar + await database.remove(cookieJar); + } + + // Update remaining jar + await database.update(chosenJar); + console.log(`[fix] Merged ${cookieJars.length} cookie jars under ${workspace.name}`); +} + +// Append .git to old git URIs to mimic previous isomorphic-git behaviour +async function _fixOldGitURIs(doc: GitRepository) { + if (!doc.uriNeedsMigration) { + return; + } + + if (!doc.uri.endsWith('.git')) { + doc.uri += '.git'; + } + + doc.uriNeedsMigration = false; + await database.update(doc); + console.log(`[fix] Fixed git URI for ${doc._id}`); +} diff --git a/packages/insomnia-app/app/common/documentation.js b/packages/insomnia-app/app/common/documentation.ts similarity index 65% rename from packages/insomnia-app/app/common/documentation.js rename to packages/insomnia-app/app/common/documentation.ts index b596ad6a11..9af6142986 100644 --- a/packages/insomnia-app/app/common/documentation.js +++ b/packages/insomnia-app/app/common/documentation.ts @@ -1,7 +1,4 @@ -// @flow -function insomniaDocs(slug: string): string { - return `https://support.insomnia.rest${slug}`; -} +const insomniaDocs = (slug: string) => `https://support.insomnia.rest${slug}`; export const docsBase = insomniaDocs('/'); export const docsGitSync = insomniaDocs('/article/193-git-sync'); @@ -9,12 +6,9 @@ export const docsTemplateTags = insomniaDocs('/article/171-template-tags'); export const docsVersionControl = insomniaDocs('/article/165-version-control-sync'); export const docsPlugins = insomniaDocs('/article/173-plugins'); export const docsImportExport = insomniaDocs('/article/172-importing-and-exporting-data'); - export const docsGitAccessToken = { - github: - 'https://docs.github.com/github/authenticating-to-github/creating-a-personal-access-token', + github: 'https://docs.github.com/github/authenticating-to-github/creating-a-personal-access-token', gitlab: 'https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html', bitbucket: 'https://support.atlassian.com/bitbucket-cloud/docs/app-passwords/', - bitbucketServer: - 'https://confluence.atlassian.com/bitbucketserver/personal-access-tokens-939515499.html', + bitbucketServer: 'https://confluence.atlassian.com/bitbucketserver/personal-access-tokens-939515499.html', }; diff --git a/packages/insomnia-app/app/common/electron-helpers.ts b/packages/insomnia-app/app/common/electron-helpers.ts new file mode 100644 index 0000000000..7d4d7d3876 --- /dev/null +++ b/packages/insomnia-app/app/common/electron-helpers.ts @@ -0,0 +1,50 @@ +import * as electron from 'electron'; +import { join } from 'path'; +import appConfig from '../../config/config.json'; +import mkdirp from 'mkdirp'; + +export function clickLink(href: string) { + electron.shell.openExternal(href); +} + +export function getDesignerDataDir() { + const { app } = electron.remote || electron; + return process.env.DESIGNER_DATA_PATH || join(app.getPath('appData'), 'Insomnia Designer'); +} + +export function getDataDirectory() { + const { app } = electron.remote || electron; + return process.env.INSOMNIA_DATA_PATH || app.getPath('userData'); +} + +export function getViewportSize(): string | null { + const { BrowserWindow } = electron.remote || electron; + const browserWindow = BrowserWindow.getFocusedWindow() || BrowserWindow.getAllWindows()[0]; + + if (browserWindow) { + const { width, height } = browserWindow.getContentBounds(); + return `${width}x${height}`; + } else { + // No windows open + return null; + } +} + +export function getScreenResolution() { + const { screen } = electron.remote || electron; + const { width, height } = screen.getPrimaryDisplay().workAreaSize; + return `${width}x${height}`; +} + +export function getUserLanguage() { + const { app } = electron.remote || electron; + return app.getLocale(); +} + +export function getTempDir() { + // NOTE: Using a fairly unique name here because "insomnia" is a common word + const { app } = electron.remote || electron; + const dir = join(app.getPath('temp'), `insomnia_${appConfig.version}`); + mkdirp.sync(dir); + return dir; +} diff --git a/packages/insomnia-app/app/common/get-workspace-label.js b/packages/insomnia-app/app/common/get-workspace-label.ts similarity index 96% rename from packages/insomnia-app/app/common/get-workspace-label.js rename to packages/insomnia-app/app/common/get-workspace-label.ts index abc6dc8ada..97f95f1e26 100644 --- a/packages/insomnia-app/app/common/get-workspace-label.js +++ b/packages/insomnia-app/app/common/get-workspace-label.ts @@ -1,4 +1,3 @@ -// @flow import type { Workspace } from '../models/workspace'; import { isDesign } from '../models/helpers/is-model'; import { strings } from './strings'; diff --git a/packages/insomnia-app/app/common/grpc-events.js b/packages/insomnia-app/app/common/grpc-events.ts similarity index 64% rename from packages/insomnia-app/app/common/grpc-events.js rename to packages/insomnia-app/app/common/grpc-events.ts index 9bf7422938..d2db5a0c35 100644 --- a/packages/insomnia-app/app/common/grpc-events.js +++ b/packages/insomnia-app/app/common/grpc-events.ts @@ -1,4 +1,4 @@ -// @flow +import { ValueOf } from 'type-fest'; export const GrpcRequestEventEnum = { start: 'GRPC_START', @@ -6,8 +6,9 @@ export const GrpcRequestEventEnum = { commit: 'GRPC_COMMIT', cancel: 'GRPC_CANCEL', cancelMultiple: 'GRPC_CANCEL_MULTIPLE', -}; -export type GrpcRequestEvent = $Values; +} as const; + +export type GrpcRequestEvent = ValueOf; export const GrpcResponseEventEnum = { start: 'GRPC_START', @@ -15,5 +16,6 @@ export const GrpcResponseEventEnum = { error: 'GRPC_ERROR', end: 'GRPC_END', status: 'GRPC_STATUS', -}; -export type GrpcResponseEvent = $Values; +} as const; + +export type GrpcResponseEvent = ValueOf; diff --git a/packages/insomnia-app/app/common/grpc-paths.js b/packages/insomnia-app/app/common/grpc-paths.ts similarity index 78% rename from packages/insomnia-app/app/common/grpc-paths.js rename to packages/insomnia-app/app/common/grpc-paths.ts index 3877defa20..4f8f2b4bda 100644 --- a/packages/insomnia-app/app/common/grpc-paths.js +++ b/packages/insomnia-app/app/common/grpc-paths.ts @@ -1,26 +1,25 @@ -// @flow - import type { GrpcMethodDefinition, GrpcMethodType } from '../network/grpc/method'; import { groupBy } from 'lodash'; import { getMethodType } from '../network/grpc/method'; - const PROTO_PATH_REGEX = /^\/(?:(?[\w.]+)\.)?(?\w+)\/(?\w+)$/; -type GrpcPathSegments = { - packageName?: string, - serviceName?: string, - methodName?: string, -}; +interface GrpcPathSegments { + packageName?: string; + serviceName?: string; + methodName?: string; +} // Split a full gRPC path into it's segments export const getGrpcPathSegments = (path: string): GrpcPathSegments => { const result = PROTO_PATH_REGEX.exec(path); - const packageName = result?.groups?.package; const serviceName = result?.groups?.service; const methodName = result?.groups?.method; - - return { packageName, serviceName, methodName }; + return { + packageName, + serviceName, + methodName, + }; }; // If all segments are found, return a shorter path, otherwise the original path @@ -31,16 +30,12 @@ export const getShortGrpcPath = ( return packageName && serviceName && methodName ? `/${serviceName}/${methodName}` : fullPath; }; -export type GrpcMethodInfo = { - segments: GrpcPathSegments, - type: GrpcMethodType, - fullPath: string, -}; - -type GroupedGrpcMethodInfo = { - [packageName: string]: Array, -}; - +export interface GrpcMethodInfo { + segments: GrpcPathSegments; + type: GrpcMethodType; + fullPath: string; +} +type GroupedGrpcMethodInfo = Record>; export const NO_PACKAGE_KEY = 'no-package'; const getMethodInfo = (method: GrpcMethodDefinition): GrpcMethodInfo => ({ diff --git a/packages/insomnia-app/app/common/har.js b/packages/insomnia-app/app/common/har.ts similarity index 60% rename from packages/insomnia-app/app/common/har.js rename to packages/insomnia-app/app/common/har.ts index 2765c8d0e1..8980a9740b 100644 --- a/packages/insomnia-app/app/common/har.js +++ b/packages/insomnia-app/app/common/har.ts @@ -1,4 +1,3 @@ -// @flow import fs from 'fs'; import clone from 'clone'; import { Cookie as toughCookie } from 'tough-cookie'; @@ -7,7 +6,7 @@ import type { RenderedRequest } from './render'; import { getRenderedRequestAndContext } from './render'; import { jarFromCookies } from 'insomnia-cookies'; import * as pluginContexts from '../plugins/context/index'; -import * as misc from './misc'; +import { getSetCookieHeaders, filterHeaders, hasAuthHeader } from './misc'; import type { Cookie } from '../models/cookie-jar'; import type { Request } from '../models/request'; import { newBodyRaw } from '../models/request'; @@ -18,173 +17,176 @@ import { RenderError } from '../templating/index'; import { smartEncodeUrl } from 'insomnia-url'; import * as plugins from '../plugins'; -export type HarCookie = { - name: string, - value: string, - path?: string, - domain?: string, - expires?: string, - httpOnly?: boolean, - secure?: boolean, - comment?: string, -}; +export interface HarCookie { + name: string; + value: string; + path?: string; + domain?: string; + expires?: string; + httpOnly?: boolean; + secure?: boolean; + comment?: string; +} -export type HarHeader = { - name: string, - value: string, - comment?: string, -}; +export interface HarHeader { + name: string; + value: string; + comment?: string; +} -export type HarQueryString = { - name: string, - value: string, - comment?: string, -}; +export interface HarQueryString { + name: string; + value: string; + comment?: string; +} -export type HarPostParam = { - name: string, - value?: string, - fileName?: string, - contentType?: string, - comment?: string, -}; +export interface HarPostParam { + name: string; + value?: string; + fileName?: string; + contentType?: string; + comment?: string; +} -export type HarPostData = { - mimeType: string, - params: Array, - text: string, - comment?: string, -}; +export interface HarPostData { + mimeType: string; + params: Array; + text: string; + comment?: string; +} -export type HarRequest = { - method: string, - url: string, - httpVersion: string, - cookies: Array, - headers: Array, - queryString: Array, - postData?: HarPostData, - headersSize: number, - bodySize: number, - comment?: string, - settingEncodeUrl: boolean, -}; +export interface HarRequest { + method: string; + url: string; + httpVersion: string; + cookies: Array; + headers: Array; + queryString: Array; + postData?: HarPostData; + headersSize: number; + bodySize: number; + comment?: string; + settingEncodeUrl: boolean; +} -export type HarContent = { - size: number, - compression?: number, - mimeType: string, - text?: string, - encoding?: string, - comment?: string, -}; +export interface HarContent { + size: number; + compression?: number; + mimeType: string; + text?: string; + encoding?: string; + comment?: string; +} -export type HarResponse = { - status: number, - statusText: string, - httpVersion: string, - cookies: Array, - headers: Array, - content: HarContent, - redirectURL: string, - headersSize: number, - bodySize: number, - comment?: string, -}; +export interface HarResponse { + status: number; + statusText: string; + httpVersion: string; + cookies: Array; + headers: Array; + content: HarContent; + redirectURL: string; + headersSize: number; + bodySize: number; + comment?: string; +} -export type HarRequestCache = { - expires?: string, - lastAccess: string, - eTag: string, - hitCount: number, - comment?: string, -}; +export interface HarRequestCache { + expires?: string; + lastAccess: string; + eTag: string; + hitCount: number; + comment?: string; +} -export type HarCache = { - beforeRequest?: HarRequestCache, - afterRequest?: HarRequestCache, - comment?: string, -}; +export interface HarCache { + beforeRequest?: HarRequestCache; + afterRequest?: HarRequestCache; + comment?: string; +} -export type HarEntryTimings = { - blocked?: number, - dns?: number, - connect?: number, - send: number, - wait: number, - receive: number, - ssl?: number, - comment?: string, -}; +export interface HarEntryTimings { + blocked?: number; + dns?: number; + connect?: number; + send: number; + wait: number; + receive: number; + ssl?: number; + comment?: string; +} -export type HarEntry = { - pageref?: string, - startedDateTime: string, - time: number, - request: HarRequest, - response: HarResponse, - cache: HarCache, - timings: HarEntryTimings, - serverIPAddress?: string, - connection?: string, - comment?: string, -}; +export interface HarEntry { + pageref?: string; + startedDateTime: string; + time: number; + request: HarRequest; + response: HarResponse; + cache: HarCache; + timings: HarEntryTimings; + serverIPAddress?: string; + connection?: string; + comment?: string; +} -export type HarPageTimings = { - onContentLoad?: number, - onLoad?: number, - comment?: string, -}; +export interface HarPageTimings { + onContentLoad?: number; + onLoad?: number; + comment?: string; +} -export type HarPage = { - startedDateTime: string, - id: string, - title: string, - pageTimings: HarPageTimings, - comment?: string, -}; +export interface HarPage { + startedDateTime: string; + id: string; + title: string; + pageTimings: HarPageTimings; + comment?: string; +} -export type HarCreator = { - name: string, - version: string, - comment?: string, -}; +export interface HarCreator { + name: string; + version: string; + comment?: string; +} -export type HarBrowser = { - name: string, - version: string, - comment?: string, -}; +export interface HarBrowser { + name: string; + version: string; + comment?: string; +} -export type HarLog = { - version: string, - creator: HarCreator, - browser?: HarBrowser, - pages?: Array, - entries: Array, - comment?: string, -}; +export interface HarLog { + version: string; + creator: HarCreator; + browser?: HarBrowser; + pages?: Array; + entries: Array; + comment?: string; +} -export type Har = { - log: HarLog, -}; +export interface Har { + log: HarLog; +} -export type ExportRequest = { - requestId: string, - environmentId: string | null, -}; +export interface ExportRequest { + requestId: string; + environmentId: string | null; +} -export async function exportHar(exportRequests: Array): Promise { +export async function exportHar(exportRequests: Array) { // Export HAR entries with the same start time in order to keep their workspace sort order. const startedDateTime = new Date().toISOString(); const entries: Array = []; + for (const exportRequest of exportRequests) { const request: Request | null = await models.request.getById(exportRequest.requestId); + if (!request) { continue; } const harRequest = await exportHarWithRequest(request, exportRequest.environmentId); + if (!harRequest) { continue; } @@ -194,6 +196,7 @@ export async function exportHar(exportRequests: Array): Promise): Promise): Promise { +export async function exportHarResponse(response: ResponseModel | null) { if (!response) { return { status: 0, @@ -249,7 +252,7 @@ export async function exportHarResponse(response: ResponseModel | null): Promise }; } - return { + const harResponse: HarResponse = { status: response.statusCode, statusText: response.statusMessage, httpVersion: 'HTTP/1.1', @@ -260,14 +263,16 @@ export async function exportHarResponse(response: ResponseModel | null): Promise headersSize: -1, bodySize: -1, }; + return harResponse; } export async function exportHarRequest( requestId: string, environmentId: string, - addContentLength: boolean = false, -): Promise { + addContentLength = false, +) { const request = await models.request.getById(requestId); + if (!request) { return null; } @@ -278,8 +283,8 @@ export async function exportHarRequest( export async function exportHarWithRequest( request: Request, environmentId: string | null, - addContentLength: boolean = false, -): Promise { + addContentLength = false, +) { try { const renderResult = await getRenderedRequestAndContext(request, environmentId); const renderedRequest = await _applyRequestPluginHooks( @@ -298,16 +303,16 @@ export async function exportHarWithRequest( async function _applyRequestPluginHooks( renderedRequest: RenderedRequest, - renderedContext: Object, -): Promise { + renderedContext: Record, +) { let newRenderedRequest = renderedRequest; + for (const { plugin, hook } of await plugins.getRequestHooks()) { newRenderedRequest = clone(newRenderedRequest); - const context = { - ...(pluginContexts.app.init(): Object), - ...(pluginContexts.request.init(newRenderedRequest, renderedContext): Object), - ...(pluginContexts.store.init(plugin): Object), + ...(pluginContexts.app.init() as Record), + ...(pluginContexts.request.init(newRenderedRequest, renderedContext) as Record), + ...(pluginContexts.store.init(plugin) as Record), }; try { @@ -323,24 +328,28 @@ async function _applyRequestPluginHooks( export async function exportHarWithRenderedRequest( renderedRequest: RenderedRequest, - addContentLength: boolean = false, -): Promise { + addContentLength = false, +) { const url = smartEncodeUrl(renderedRequest.url, renderedRequest.settingEncodeUrl); if (addContentLength) { const hasContentLengthHeader = - misc.filterHeaders(renderedRequest.headers, 'Content-Length').length > 0; + filterHeaders(renderedRequest.headers, 'Content-Length').length > 0; if (!hasContentLengthHeader) { const name = 'Content-Length'; const value = Buffer.byteLength((renderedRequest.body || {}).text || '').toString(); - renderedRequest.headers.push({ name, value }); + renderedRequest.headers.push({ + name, + value, + }); } } // Set auth header if we have it - if (!misc.hasAuthHeader(renderedRequest.headers)) { + if (!hasAuthHeader(renderedRequest.headers)) { const header = await getAuthHeader(renderedRequest, url); + if (header) { renderedRequest.headers.push({ name: header.name, @@ -349,7 +358,7 @@ export async function exportHarWithRenderedRequest( } } - return { + const harRequest: HarRequest = { method: renderedRequest.method, url, httpVersion: 'HTTP/1.1', @@ -361,32 +370,39 @@ export async function exportHarWithRenderedRequest( bodySize: -1, settingEncodeUrl: renderedRequest.settingEncodeUrl, }; + return harRequest; } -function getRequestCookies(renderedRequest: RenderedRequest): Array { +function getRequestCookies(renderedRequest: RenderedRequest) { const jar = jarFromCookies(renderedRequest.cookieJar.cookies); const domainCookies = jar.getCookiesSync(renderedRequest.url); - return domainCookies.map(mapCookie); + const harCookies: Array = domainCookies.map(mapCookie); + return harCookies; } -function getReponseCookies(response: ResponseModel): Array { - return misc - .getSetCookieHeaders(response.headers) - .map(h => { - let cookie; - try { - cookie = toughCookie.parse(h.value || ''); - } catch (error) {} - if (!cookie) { - return null; - } +function getReponseCookies(response: ResponseModel) { + const headers = response.headers.filter(Boolean) as Array; + const responseCookies = getSetCookieHeaders(headers) + .reduce((accumulator, harCookie) => { + let cookie: null | undefined | toughCookie = null; - return mapCookie(cookie); - }) - .filter(Boolean); + try { + cookie = toughCookie.parse(harCookie.value || ''); + } catch (error) {} + + if (cookie === null || cookie === undefined) { + return accumulator; + } + + return [ + ...accumulator, + mapCookie(cookie as unknown as Cookie), + ]; + }, [] as Array); + return responseCookies; } -function mapCookie(cookie: Cookie): HarCookie { +function mapCookie(cookie: Cookie) { const harCookie: HarCookie = { name: cookie.key, value: cookie.value, @@ -401,7 +417,8 @@ function mapCookie(cookie: Cookie): HarCookie { } if (cookie.expires) { - let expires = null; + let expires: Date | null = null; + if (cookie.expires instanceof Date) { expires = cookie.expires; } else if (typeof cookie.expires === 'string') { @@ -427,59 +444,55 @@ function mapCookie(cookie: Cookie): HarCookie { return harCookie; } -function getResponseContent(response: ResponseModel): HarContent { - let body: Buffer | null = models.response.getBodyBuffer(response); +function getResponseContent(response: ResponseModel) { + let body = models.response.getBodyBuffer(response); if (body === null) { body = Buffer.alloc(0); } - return { + const harContent: HarContent = { size: body.byteLength, mimeType: response.contentType, text: body.toString('utf8'), }; + return harContent; } -function getResponseHeaders(response: ResponseModel): Array { +function getResponseHeaders(response: ResponseModel) { return response.headers .filter(header => header.name) - .map(h => { - return { - name: h.name, - value: h.value, - }; - }); + .map(header => ({ + name: header.name, + value: header.value, + })); } -function getRequestHeaders(renderedRequest: RenderedRequest): Array { +function getRequestHeaders(renderedRequest: RenderedRequest) { return renderedRequest.headers .filter(header => header.name) - .map(header => { - return { - name: header.name, - value: header.value, - }; - }); + .map(header => ({ + name: header.name, + value: header.value, + })); } function getRequestQueryString(renderedRequest: RenderedRequest): Array { - return renderedRequest.parameters.map(parameter => { - return { - name: parameter.name, - value: parameter.value, - }; - }); + return renderedRequest.parameters.map(parameter => ({ + name: parameter.name, + value: parameter.value, + })); } -function getRequestPostData(renderedRequest: RenderedRequest): HarPostData | void { +function getRequestPostData(renderedRequest: RenderedRequest): HarPostData | undefined { let body; + if (renderedRequest.body.fileName) { try { body = newBodyRaw(fs.readFileSync(renderedRequest.body.fileName, 'base64')); } catch (e) { console.warn('[code gen] Failed to read file', e); - return undefined; + return; } } else { // For every other type, Insomnia uses the same body format as HAR @@ -487,6 +500,7 @@ function getRequestPostData(renderedRequest: RenderedRequest): HarPostData | voi } let params = []; + if (body.params) { params = body.params.map(param => { if (param.type === 'file') { diff --git a/packages/insomnia-app/app/common/hotkeys-listener.js b/packages/insomnia-app/app/common/hotkeys-listener.ts similarity index 58% rename from packages/insomnia-app/app/common/hotkeys-listener.js rename to packages/insomnia-app/app/common/hotkeys-listener.ts index cf9f59b486..7258704d89 100644 --- a/packages/insomnia-app/app/common/hotkeys-listener.js +++ b/packages/insomnia-app/app/common/hotkeys-listener.ts @@ -1,15 +1,14 @@ -// @flow import type { HotKeyDefinition, KeyBindings, KeyCombination } from './hotkeys'; import { areSameKeyCombinations, getPlatformKeyCombinations } from './hotkeys'; import * as models from '../models'; -function _pressedHotKey(e: KeyboardEvent, bindings: KeyBindings): boolean { +const _pressedHotKey = (event: KeyboardEvent, bindings: KeyBindings) => { const pressedKeyComb: KeyCombination = { - ctrl: e.ctrlKey, - alt: e.altKey, - shift: e.shiftKey, - meta: e.metaKey, - keyCode: e.keyCode, + ctrl: event.ctrlKey, + alt: event.altKey, + shift: event.shiftKey, + meta: event.metaKey, + keyCode: event.keyCode, }; const keyCombList = getPlatformKeyCombinations(bindings); @@ -20,35 +19,33 @@ function _pressedHotKey(e: KeyboardEvent, bindings: KeyBindings): boolean { } return false; -} +}; /** * Check whether a hotkey has been pressed. - * @param e the activated keyboard event. + * @param event the activated keyboard event. * @param definition the hotkey definition being checked. - * @returns {Promise} */ -export async function pressedHotKey( - e: KeyboardEvent, +export const pressedHotKey = async ( + event: KeyboardEvent, definition: HotKeyDefinition, -): Promise { +) => { const settings = await models.settings.getOrCreate(); - return _pressedHotKey(e, settings.hotKeyRegistry[definition.id]); -} + return _pressedHotKey(event, settings.hotKeyRegistry[definition.id]); +}; /** * Call callback if the hotkey has been pressed. - * @param e the activated keyboard event. + * @param event the activated keyboard event. * @param definition the hotkey definition being checked. * @param callback to be called if the hotkey has been activated. - * @returns {Promise} */ -export async function executeHotKey( - e: KeyboardEvent, +export const executeHotKey = async ( + event: KeyboardEvent, definition: HotKeyDefinition, - callback: Function, -): Promise { - if (await pressedHotKey(e, definition)) { + callback: T, +) => { + if (await pressedHotKey(event, definition)) { callback(); } -} +}; diff --git a/packages/insomnia-app/app/common/hotkeys.js b/packages/insomnia-app/app/common/hotkeys.ts similarity index 94% rename from packages/insomnia-app/app/common/hotkeys.js rename to packages/insomnia-app/app/common/hotkeys.ts index d4e407c32a..ec31dd74a2 100644 --- a/packages/insomnia-app/app/common/hotkeys.js +++ b/packages/insomnia-app/app/common/hotkeys.ts @@ -1,4 +1,3 @@ -// @flow import { keyboardKeys } from './keyboard-keys'; import { ALT_SYM, CTRL_SYM, isMac, META_SYM, SHIFT_SYM } from './constants'; import { strings } from './strings'; @@ -7,38 +6,36 @@ import { strings } from './strings'; * The readable definition of a hotkey. * The {@code id} the hotkey's reference id. */ -export type HotKeyDefinition = { - id: string, - description: string, -}; +export interface HotKeyDefinition { + id: string; + description: string; +} /** * The combination of key presses that will activate a hotkey if pressed. */ -export type KeyCombination = { - ctrl: boolean, - alt: boolean, - shift: boolean, - meta: boolean, - keyCode: number, -}; +export interface KeyCombination { + ctrl: boolean; + alt: boolean; + shift: boolean; + meta: boolean; + keyCode: number; +} /** * The collection of a hotkey's key combinations for each platforms. */ -export type KeyBindings = { - macKeys: Array, +export interface KeyBindings { + macKeys: Array; // The key combinations for both Windows and Linux. - winLinuxKeys: Array, -}; + winLinuxKeys: Array; +} /** * The collection of defined hotkeys. * The registry maps a hotkey by its reference id to its key bindings. */ -export type HotKeyRegistry = { - [refId: string]: KeyBindings, -}; +export type HotKeyRegistry = Record; function defineHotKey(id: string, description: string): HotKeyDefinition { return { @@ -85,84 +82,53 @@ function keyBinds( * The collection of available hotkeys' and their definitions. */ // Not using dot, because NeDB prohibits field names to contain dots. -export const hotKeyRefs: { [string]: HotKeyDefinition } = { +export const hotKeyRefs: Record = { WORKSPACE_SHOW_SETTINGS: defineHotKey( 'workspace_showSettings', `Show ${strings.document} / ${strings.collection} Settings`, ), - REQUEST_SHOW_SETTINGS: defineHotKey('request_showSettings', 'Show Request Settings'), - PREFERENCES_SHOW_KEYBOARD_SHORTCUTS: defineHotKey( 'preferences_showKeyboardShortcuts', 'Show Keyboard Shortcuts', ), - PREFERENCES_SHOW_GENERAL: defineHotKey('preferences_showGeneral', 'Show App Preferences'), - TOGGLE_MAIN_MENU: defineHotKey('toggleMainMenu', 'Toggle Main Menu'), - REQUEST_QUICK_SWITCH: defineHotKey('request_quickSwitch', 'Switch Requests'), - SHOW_RECENT_REQUESTS: defineHotKey('request_showRecent', 'Show Recent Requests'), - SHOW_RECENT_REQUESTS_PREVIOUS: defineHotKey( 'request_showRecentPrevious', 'Show Recent Requests (Previous)', ), - PLUGIN_RELOAD: defineHotKey('plugin_reload', 'Reload Plugins'), - SHOW_AUTOCOMPLETE: defineHotKey('showAutocomplete', 'Show Autocomplete'), - REQUEST_SEND: defineHotKey('request_send', 'Send Request'), - REQUEST_SHOW_OPTIONS: defineHotKey('request_showOptions', 'Send Request (Options)'), - ENVIRONMENT_SHOW_EDITOR: defineHotKey('environment_showEditor', 'Show Environment Editor'), - ENVIRONMENT_SHOW_SWITCH_MENU: defineHotKey('environment_showSwitchMenu', 'Switch Environments'), - REQUEST_TOGGLE_HTTP_METHOD_MENU: defineHotKey( 'request_toggleHttpMethodMenu', 'Change HTTP Method', ), - REQUEST_TOGGLE_HISTORY: defineHotKey('request_toggleHistory', 'Show Request History'), - REQUEST_FOCUS_URL: defineHotKey('request_focusUrl', 'Focus URL'), - REQUEST_SHOW_GENERATE_CODE_EDITOR: defineHotKey( 'request_showGenerateCodeEditor', 'Generate Code', ), - SIDEBAR_FOCUS_FILTER: defineHotKey('sidebar_focusFilter', 'Filter Sidebar'), - SIDEBAR_TOGGLE: defineHotKey('sidebar_toggle', 'Toggle Sidebar'), - RESPONSE_FOCUS: defineHotKey('response_focus', 'Focus Response'), - SHOW_COOKIES_EDITOR: defineHotKey('showCookiesEditor', 'Edit Cookies'), - REQUEST_SHOW_CREATE: defineHotKey('request_showCreate', 'Create Request'), - REQUEST_QUICK_CREATE: defineHotKey('request_quickCreate', 'Create Request (Quick)'), - REQUEST_SHOW_DELETE: defineHotKey('request_showDelete', 'Delete Request'), - REQUEST_SHOW_CREATE_FOLDER: defineHotKey('request_showCreateFolder', 'Create Folder'), - REQUEST_SHOW_DUPLICATE: defineHotKey('request_showDuplicate', 'Duplicate Request'), - REQUEST_TOGGLE_PIN: defineHotKey('request_togglePin', 'Pin/Unpin Request'), - CLOSE_DROPDOWN: defineHotKey('closeDropdown', 'Close Dropdown'), - CLOSE_MODAL: defineHotKey('closeModal', 'Close Modal'), - ENVIRONMENT_UNCOVER_VARIABLES: defineHotKey('environment_uncoverVariables', 'Uncover Variables'), - // Designer-specific SHOW_SPEC_EDITOR: defineHotKey('activity_specEditor', 'Show Spec Activity'), SHOW_TEST: defineHotKey('activity_test', 'Show Test Activity'), @@ -179,52 +145,42 @@ const defaultRegistry: HotKeyRegistry = { keyComb(false, false, true, true, keyboardKeys.comma.keyCode), keyComb(true, false, true, false, keyboardKeys.comma.keyCode), ), - [hotKeyRefs.REQUEST_SHOW_SETTINGS.id]: keyBinds( keyComb(false, true, true, true, keyboardKeys.comma.keyCode), keyComb(true, true, true, false, keyboardKeys.comma.keyCode), ), - [hotKeyRefs.PREFERENCES_SHOW_KEYBOARD_SHORTCUTS.id]: keyBinds( keyComb(true, false, true, true, keyboardKeys.forwardslash.keyCode), keyComb(true, false, true, false, keyboardKeys.forwardslash.keyCode), ), - [hotKeyRefs.PREFERENCES_SHOW_GENERAL.id]: keyBinds( keyComb(false, false, false, true, keyboardKeys.comma.keyCode), keyComb(true, false, false, false, keyboardKeys.comma.keyCode), ), - [hotKeyRefs.TOGGLE_MAIN_MENU.id]: keyBinds( keyComb(false, true, false, true, keyboardKeys.comma.keyCode), keyComb(true, true, false, false, keyboardKeys.comma.keyCode), ), - [hotKeyRefs.REQUEST_QUICK_SWITCH.id]: keyBinds( keyComb(false, false, false, true, keyboardKeys.p.keyCode), keyComb(true, false, false, false, keyboardKeys.p.keyCode), ), - [hotKeyRefs.SHOW_RECENT_REQUESTS.id]: keyBinds( keyComb(true, false, false, false, keyboardKeys.tab.keyCode), keyComb(true, false, false, false, keyboardKeys.tab.keyCode), ), - [hotKeyRefs.SHOW_RECENT_REQUESTS_PREVIOUS.id]: keyBinds( keyComb(true, false, true, false, keyboardKeys.tab.keyCode), keyComb(true, false, true, false, keyboardKeys.tab.keyCode), ), - [hotKeyRefs.PLUGIN_RELOAD.id]: keyBinds( keyComb(false, false, true, true, keyboardKeys.r.keyCode), keyComb(true, false, true, false, keyboardKeys.r.keyCode), ), - [hotKeyRefs.SHOW_AUTOCOMPLETE.id]: keyBinds( keyComb(true, false, false, false, keyboardKeys.space.keyCode), keyComb(true, false, false, false, keyboardKeys.space.keyCode), ), - [hotKeyRefs.REQUEST_SEND.id]: keyBinds( [ keyComb(false, false, false, true, keyboardKeys.enter.keyCode), @@ -237,127 +193,102 @@ const defaultRegistry: HotKeyRegistry = { keyComb(false, false, false, false, keyboardKeys.f5.keyCode), ], ), - [hotKeyRefs.REQUEST_SHOW_OPTIONS.id]: keyBinds( keyComb(false, false, true, true, keyboardKeys.enter.keyCode), keyComb(true, false, true, false, keyboardKeys.enter.keyCode), ), - [hotKeyRefs.ENVIRONMENT_SHOW_EDITOR.id]: keyBinds( keyComb(false, false, false, true, keyboardKeys.e.keyCode), keyComb(true, false, false, false, keyboardKeys.e.keyCode), ), - [hotKeyRefs.ENVIRONMENT_SHOW_SWITCH_MENU.id]: keyBinds( keyComb(false, false, true, true, keyboardKeys.e.keyCode), keyComb(true, false, true, false, keyboardKeys.e.keyCode), ), - [hotKeyRefs.REQUEST_TOGGLE_HTTP_METHOD_MENU.id]: keyBinds( keyComb(false, false, true, true, keyboardKeys.l.keyCode), keyComb(true, false, true, false, keyboardKeys.l.keyCode), ), - [hotKeyRefs.REQUEST_TOGGLE_HISTORY.id]: keyBinds( keyComb(false, false, true, true, keyboardKeys.h.keyCode), keyComb(true, false, true, false, keyboardKeys.h.keyCode), ), - [hotKeyRefs.REQUEST_FOCUS_URL.id]: keyBinds( keyComb(false, false, false, true, keyboardKeys.l.keyCode), keyComb(true, false, false, false, keyboardKeys.l.keyCode), ), - [hotKeyRefs.REQUEST_SHOW_GENERATE_CODE_EDITOR.id]: keyBinds( keyComb(false, false, true, true, keyboardKeys.g.keyCode), keyComb(true, false, true, false, keyboardKeys.g.keyCode), ), - [hotKeyRefs.SIDEBAR_FOCUS_FILTER.id]: keyBinds( keyComb(false, false, true, true, keyboardKeys.f.keyCode), keyComb(true, false, true, false, keyboardKeys.f.keyCode), ), - [hotKeyRefs.SIDEBAR_TOGGLE.id]: keyBinds( keyComb(false, false, false, true, keyboardKeys.backslash.keyCode), keyComb(true, false, false, false, keyboardKeys.backslash.keyCode), ), - [hotKeyRefs.RESPONSE_FOCUS.id]: keyBinds( keyComb(false, false, false, true, keyboardKeys.singlequote.keyCode), keyComb(true, false, false, false, keyboardKeys.singlequote.keyCode), ), - [hotKeyRefs.SHOW_COOKIES_EDITOR.id]: keyBinds( keyComb(false, false, false, true, keyboardKeys.k.keyCode), keyComb(true, false, false, false, keyboardKeys.k.keyCode), ), - [hotKeyRefs.REQUEST_SHOW_CREATE.id]: keyBinds( keyComb(false, false, false, true, keyboardKeys.n.keyCode), keyComb(true, false, false, false, keyboardKeys.n.keyCode), ), - [hotKeyRefs.REQUEST_QUICK_CREATE.id]: keyBinds( keyComb(false, true, false, true, keyboardKeys.n.keyCode), keyComb(true, true, false, false, keyboardKeys.n.keyCode), ), - [hotKeyRefs.REQUEST_SHOW_DELETE.id]: keyBinds( keyComb(false, false, true, true, keyboardKeys.delete.keyCode), keyComb(true, false, true, false, keyboardKeys.delete.keyCode), ), - [hotKeyRefs.REQUEST_SHOW_CREATE_FOLDER.id]: keyBinds( keyComb(false, false, true, true, keyboardKeys.n.keyCode), keyComb(true, false, true, false, keyboardKeys.n.keyCode), ), - [hotKeyRefs.REQUEST_SHOW_DUPLICATE.id]: keyBinds( keyComb(false, false, false, true, keyboardKeys.d.keyCode), keyComb(true, false, false, false, keyboardKeys.d.keyCode), ), - [hotKeyRefs.REQUEST_TOGGLE_PIN.id]: keyBinds( keyComb(false, false, true, true, keyboardKeys.p.keyCode), keyComb(true, false, true, false, keyboardKeys.p.keyCode), ), - [hotKeyRefs.CLOSE_DROPDOWN.id]: keyBinds( keyComb(false, false, false, false, keyboardKeys.esc.keyCode), keyComb(false, false, false, false, keyboardKeys.esc.keyCode), ), - [hotKeyRefs.CLOSE_MODAL.id]: keyBinds( keyComb(false, false, false, false, keyboardKeys.esc.keyCode), keyComb(false, false, false, false, keyboardKeys.esc.keyCode), ), - [hotKeyRefs.ENVIRONMENT_UNCOVER_VARIABLES.id]: keyBinds( keyComb(false, true, true, false, keyboardKeys.u.keyCode), keyComb(false, true, true, false, keyboardKeys.u.keyCode), ), - [hotKeyRefs.SHOW_SPEC_EDITOR.id]: keyBinds( keyComb(false, false, true, true, keyboardKeys.s.keyCode), keyComb(true, false, true, false, keyboardKeys.s.keyCode), ), - [hotKeyRefs.SHOW_TEST.id]: keyBinds( keyComb(false, false, true, true, keyboardKeys.t.keyCode), keyComb(true, false, true, false, keyboardKeys.t.keyCode), ), - [hotKeyRefs.SHOW_MONITOR.id]: keyBinds( keyComb(false, false, true, true, keyboardKeys.m.keyCode), keyComb(true, false, true, false, keyboardKeys.m.keyCode), ), - [hotKeyRefs.SHOW_HOME.id]: keyBinds( keyComb(false, false, true, true, keyboardKeys.h.keyCode), keyComb(true, false, true, false, keyboardKeys.h.keyCode), ), - [hotKeyRefs.FILTER_DOCUMENTS.id]: keyBinds( keyComb(false, false, false, true, keyboardKeys.f.keyCode), keyComb(true, false, false, false, keyboardKeys.f.keyCode), @@ -391,12 +322,15 @@ export function newDefaultKeyBindings(hotKeyRefId: string): KeyBindings { */ export function newDefaultRegistry(): HotKeyRegistry { const newDefaults: HotKeyRegistry = {}; + for (const refId in defaultRegistry) { if (!defaultRegistry.hasOwnProperty(refId)) { continue; } + newDefaults[refId] = newDefaultKeyBindings(refId); } + return newDefaults; } @@ -409,6 +343,7 @@ export function getPlatformKeyCombinations(bindings: KeyBindings): Array { if (areSameKeyCombinations(keyComb, defKeyComb)) { return true; } + return false; }); + if (found == null) { return false; } } + return true; } @@ -462,7 +402,7 @@ export function areKeyBindingsSameAsDefault(hotKeyRefId: string, keyBinds: KeyBi * @param keyCode * @returns {string} */ -export function getChar(keyCode: number): string { +export function getChar(keyCode: number) { let char; const key = Object.keys(keyboardKeys).find(k => keyboardKeys[k].keyCode === keyCode); @@ -475,10 +415,11 @@ export function getChar(keyCode: number): string { return char || 'unknown'; } -function joinHotKeys(mustUsePlus: boolean, keys: Array): string { +function joinHotKeys(mustUsePlus: boolean, keys: Array) { if (!mustUsePlus && isMac()) { return keys.join(''); } + return keys.join('+'); } @@ -487,12 +428,11 @@ function joinHotKeys(mustUsePlus: boolean, keys: Array): string { * @param keyCode * @returns {boolean} */ -export function isModifierKeyCode(keyCode: number): boolean { +export function isModifierKeyCode(keyCode: number) { return ( keyCode === keyboardKeys.alt.keyCode || keyCode === keyboardKeys.shift.keyCode || - keyCode === keyboardKeys.ctrl.keyCode || - // Meta keys. + keyCode === keyboardKeys.ctrl.keyCode || // Meta keys. keyCode === keyboardKeys.leftwindowkey.keyCode || keyCode === keyboardKeys.rightwindowkey.keyCode || keyCode === keyboardKeys.selectkey.keyCode @@ -512,22 +452,24 @@ export function isModifierKeyCode(keyCode: number): boolean { export function constructKeyCombinationDisplay( keyComb: KeyCombination, mustUsePlus: boolean, -): string { +) { const { ctrl, alt, shift, meta, keyCode } = keyComb; - const chars = []; - + const chars: Array = []; alt && chars.push(ALT_SYM); shift && chars.push(SHIFT_SYM); ctrl && chars.push(CTRL_SYM); meta && chars.push(META_SYM); + if (keyCode != null && !isModifierKeyCode(keyCode)) { chars.push(getChar(keyCode)); } let joint = joinHotKeys(mustUsePlus, chars); + if (mustUsePlus && isModifierKeyCode(keyCode)) { joint += '+'; } + return joint; } @@ -544,12 +486,14 @@ export function getHotKeyDisplay( hotKeyRegistry: HotKeyRegistry, mustUsePlus: boolean, ) { - const hotKey: ?KeyBindings = hotKeyRegistry[hotKeyDef.id]; + const hotKey: KeyBindings | null | undefined = hotKeyRegistry[hotKeyDef.id]; + if (!hotKey) { return ''; } const keyCombs: Array = getPlatformKeyCombinations(hotKey); + if (keyCombs.length === 0) { return ''; } diff --git a/packages/insomnia-app/app/common/import.js b/packages/insomnia-app/app/common/import.ts similarity index 78% rename from packages/insomnia-app/app/common/import.js rename to packages/insomnia-app/app/common/import.ts index d43eaa8364..55ad2eb37e 100644 --- a/packages/insomnia-app/app/common/import.js +++ b/packages/insomnia-app/app/common/import.ts @@ -1,7 +1,6 @@ -// @flow -import { convert } from 'insomnia-importers'; +import { convert, Insomnia4Data } from 'insomnia-importers'; import clone from 'clone'; -import * as db from './database'; +import { database as db } from './database'; import * as har from './har'; import type { BaseModel } from '../models/index'; import * as models from '../models/index'; @@ -20,14 +19,13 @@ import { isRequestGroup, isWorkspace, } from '../models/helpers/is-model'; -import type { Workspace, WorkspaceScope } from '../models/workspace'; +import type { Workspace } from '../models/workspace'; import type { ApiSpec } from '../models/api-spec'; +import { ImportToWorkspacePrompt, SetWorkspaceScopePrompt } from '../ui/redux/modules/helpers'; const WORKSPACE_ID_KEY = '__WORKSPACE_ID__'; const BASE_ENVIRONMENT_ID_KEY = '__BASE_ENVIRONMENT_ID__'; - const EXPORT_FORMAT = 4; - const EXPORT_TYPE_REQUEST = 'request'; const EXPORT_TYPE_GRPC_REQUEST = 'grpc_request'; const EXPORT_TYPE_REQUEST_GROUP = 'request_group'; @@ -39,10 +37,8 @@ const EXPORT_TYPE_ENVIRONMENT = 'environment'; const EXPORT_TYPE_API_SPEC = 'api_spec'; const EXPORT_TYPE_PROTO_FILE = 'proto_file'; const EXPORT_TYPE_PROTO_DIRECTORY = 'proto_directory'; - // If we come across an ID of this form, we will replace it with a new one const REPLACE_ID_REGEX = /__\w+_\d+__/g; - const MODELS = { [EXPORT_TYPE_REQUEST]: models.request, [EXPORT_TYPE_GRPC_REQUEST]: models.grpcRequest, @@ -57,40 +53,40 @@ const MODELS = { [EXPORT_TYPE_PROTO_DIRECTORY]: models.protoDirectory, }; -export type ImportResult = { - source: string, - error: Error | null, - summary: { [string]: Array }, -}; +export interface ImportResult { + source: string; + error: Error | null; + summary: Record>; +} -type ConvertResultType = { - id: string, - name: string, - description: string, -}; +interface ConvertResultType { + id: string; + name: string; + description: string; +} -type ConvertResult = { - type: ConvertResultType, +interface ConvertResult { + type: ConvertResultType; data: { - resources: Array, - }, -}; + resources: Array>; + }; +} -export type ImportRawConfig = { - getWorkspaceId: () => Promise, - getWorkspaceScope?: string => Promise, - enableDiffBasedPatching?: boolean, - enableDiffDeep?: boolean, +export interface ImportRawConfig { + getWorkspaceId: ImportToWorkspacePrompt; + getWorkspaceScope?: SetWorkspaceScopePrompt; + enableDiffBasedPatching?: boolean; + enableDiffDeep?: boolean; bypassDiffProps?: { - url: string, - }, -}; + url: boolean; + }; +} -export async function importUri(uri: string, importConfig: ImportRawConfig): Promise { +export async function importUri(uri: string, importConfig: ImportRawConfig) { let rawText; - // If GH preview, force raw const url = new URL(uri); + if (url.origin === 'https://github.com') { uri = uri .replace('https://github.com', 'https://raw.githubusercontent.com') @@ -114,6 +110,7 @@ export async function importUri(uri: string, importConfig: ImportRawConfig): Pro if (error) { showError({ title: 'Failed to import', + // @ts-expect-error -- TSCONVERSION appears to be a genuine error error: error.message, message: 'Import failed', }); @@ -127,15 +124,18 @@ export async function importUri(uri: string, importConfig: ImportRawConfig): Pro return count === 0 ? null : `${count} ${name}`; }) .filter(s => s !== null); - let message; + if (statements.length === 0) { message = 'Nothing was found to import.'; } else { message = `You imported ${statements.join(', ')}!`; } - showModal(AlertModal, { title: 'Import Succeeded', message }); + showModal(AlertModal, { + title: 'Import Succeeded', + message, + }); return result; } @@ -148,22 +148,24 @@ export async function importRaw( enableDiffDeep, bypassDiffProps, }: ImportRawConfig, -): Promise { +) { let results: ConvertResult; + try { results = await convert(rawContent); } catch (err) { - return { + const importResult: ImportResult = { source: 'not found', error: err, summary: {}, }; + return importResult; } const { data, type: resultsType } = results; - // Generate all the ids we may need - const generatedIds: { [string]: string | Function } = {}; + const generatedIds: Record) => any)> = {}; + for (const r of data.resources) { for (const key of r._id.match(REPLACE_ID_REGEX) || []) { generatedIds[key] = generateId(MODELS[r._type].prefix); @@ -173,15 +175,11 @@ export async function importRaw( // Contains the ID of the workspace to be used with the import generatedIds[WORKSPACE_ID_KEY] = async () => { const workspaceId = await getWorkspaceId(); - // First try getting the workspace to overwrite const workspace = await models.workspace.getById(workspaceId || 'n/a'); - // Update this fn so it doesn't run again const idToUse = workspace?._id || generateId(models.workspace.prefix); - generatedIds[WORKSPACE_ID_KEY] = idToUse; - return idToUse; }; @@ -189,17 +187,15 @@ export async function importRaw( generatedIds[BASE_ENVIRONMENT_ID_KEY] = async () => { const parentId = await fnOrString(generatedIds[WORKSPACE_ID_KEY]); const baseEnvironment = await models.environment.getOrCreateForWorkspaceId(parentId); - // Update this fn so it doesn't run again generatedIds[BASE_ENVIRONMENT_ID_KEY] = baseEnvironment._id; - return baseEnvironment._id; }; // Import everything backwards so they get inserted in the correct order data.resources.reverse(); - const importedDocs = {}; + for (const model of models.all()) { importedDocs[model.type] = []; } @@ -227,7 +223,8 @@ export async function importRaw( } } - const model: Object = MODELS[resource._type]; + const model = MODELS[resource._type]; + if (!model) { console.warn('Unknown doc type for import', resource._type); continue; @@ -256,7 +253,10 @@ export async function importRaw( ) { try { JSON.parse(resource.body.text); - resource.headers.push({ name: 'Content-Type', value: 'application/json' }); + resource.headers.push({ + name: 'Content-Type', + value: 'application/json', + }); } catch (err) { // Not JSON } @@ -264,6 +264,7 @@ export async function importRaw( const existingDoc = await db.get(model.type, resource._id); let newDoc: BaseModel; + if (existingDoc) { let updateDoc = resource; @@ -279,20 +280,23 @@ export async function importRaw( // If workspace, don't overwrite the existing scope if (isWorkspace(model)) { - (updateDoc: Workspace).scope = (existingDoc: Workspace).scope; + (updateDoc as Workspace).scope = (existingDoc as Workspace).scope; } newDoc = await db.docUpdate(existingDoc, updateDoc); } else { if (isWorkspace(model)) { - await updateWorkspaceScope(resource, resultsType, getWorkspaceScope); + await updateWorkspaceScope(resource as Workspace, resultsType, getWorkspaceScope); } + newDoc = await db.docCreate(model.type, resource); // Mark as not seen if we created a new workspace from sync if (isWorkspace(newDoc)) { const workspaceMeta = await models.workspaceMeta.getOrCreateByParentId(newDoc._id); - await models.workspaceMeta.update(workspaceMeta, { hasSeen: false }); + await models.workspaceMeta.update(workspaceMeta, { + hasSeen: false, + }); } } @@ -306,13 +310,13 @@ export async function importRaw( contents: rawContent, contentType: 'yaml', }); - importedDocs[spec.type].push(spec); } // Set active environment when none is currently selected and one exists const meta = await models.workspaceMeta.getOrCreateByParentId(workspace._id); const envs = importedDocs[models.environment.type]; + if (!meta.activeEnvironmentId && envs.length > 0) { meta.activeEnvironmentId = envs[0]._id; await models.workspaceMeta.update(meta); @@ -320,20 +324,19 @@ export async function importRaw( } await db.flushChanges(); - trackEvent('Data', 'Import', resultsType.id); - - return { + const importRequest: ImportResult = { source: resultsType && typeof resultsType.id === 'string' ? resultsType.id : 'unknown', summary: importedDocs, error: null, }; + return importRequest; } async function updateWorkspaceScope( resource: Workspace, resultType: ConvertResultType, - getWorkspaceScope?: string => Promise, + getWorkspaceScope?: SetWorkspaceScopePrompt, ) { // Set the workspace scope if creating a new workspace // IF is creating a new workspace @@ -342,6 +345,7 @@ async function updateWorkspaceScope( if ((!resource.hasOwnProperty('scope') || resource.scope === null) && getWorkspaceScope) { const workspaceName = resource.name; let specName; + // If is from insomnia v4 and the spec has contents, add to the name when prompting if (isInsomniaV4Import(resultType)) { const spec: ApiSpec | null = await models.apiSpec.getByParentId(resource._id); @@ -350,23 +354,24 @@ async function updateWorkspaceScope( specName = spec.fileName; } } + const nameToPrompt = specName ? `${specName} / ${workspaceName}` : workspaceName; - (resource: Workspace).scope = await getWorkspaceScope(nameToPrompt); + (resource as Workspace).scope = await getWorkspaceScope(nameToPrompt); } } -export function isApiSpecImport({ id }: ConvertResultType): boolean { +export function isApiSpecImport({ id }: ConvertResultType) { return id === 'openapi3' || id === 'swagger2'; } -export function isInsomniaV4Import({ id }: ConvertResultType): boolean { +export function isInsomniaV4Import({ id }: ConvertResultType) { return id === 'insomnia-4'; } export async function exportWorkspacesHAR( parentDoc: BaseModel | null = null, - includePrivateDocs: boolean = false, -): Promise { + includePrivateDocs = false, +) { const docs: Array = await getDocWithDescendants(parentDoc, includePrivateDocs); const requests: Array = docs.filter(isRequest); return exportRequestsHAR(requests, includePrivateDocs); @@ -374,11 +379,12 @@ export async function exportWorkspacesHAR( export async function exportRequestsHAR( requests: Array, - includePrivateDocs: boolean = false, -): Promise { + includePrivateDocs = false, +) { const workspaces: Array = []; - const mapRequestIdToWorkspace: Object = {}; - const workspaceLookup: Object = {}; + const mapRequestIdToWorkspace: Record = {}; + const workspaceLookup: Record = {}; + for (const request of requests) { const ancestors: Array = await db.withAncestors(request, [ models.workspace.type, @@ -386,32 +392,42 @@ export async function exportRequestsHAR( ]); const workspace = ancestors.find(isWorkspace); mapRequestIdToWorkspace[request._id] = workspace; + if (workspace == null || workspaceLookup.hasOwnProperty(workspace._id)) { continue; } + workspaceLookup[workspace._id] = true; workspaces.push(workspace); } - const mapWorkspaceIdToEnvironmentId: Object = {}; + const mapWorkspaceIdToEnvironmentId: Record = {}; + for (const workspace of workspaces) { const workspaceMeta = await models.workspaceMeta.getByParentId(workspace._id); let environmentId = workspaceMeta ? workspaceMeta.activeEnvironmentId : null; const environment = await models.environment.getById(environmentId || 'n/a'); + if (!environment || (environment.isPrivate && !includePrivateDocs)) { environmentId = 'n/a'; } + mapWorkspaceIdToEnvironmentId[workspace._id] = environmentId; } - requests = requests.sort((a: Object, b: Object) => (a.metaSortKey < b.metaSortKey ? -1 : 1)); - const harRequests: Array = []; + requests = requests.sort((a: Record, b: Record) => + a.metaSortKey < b.metaSortKey ? -1 : 1, + ); + const harRequests: Array = []; + for (const request of requests) { const workspace = mapRequestIdToWorkspace[request._id]; + if (workspace == null) { // Workspace not found for request, so don't export it. continue; } + const environmentId = mapWorkspaceIdToEnvironmentId[workspace._id]; harRequests.push({ requestId: request._id, @@ -420,9 +436,7 @@ export async function exportRequestsHAR( } const data = await har.exportHar(harRequests); - trackEvent('Data', 'Export', 'HAR'); - return JSON.stringify(data, null, '\t'); } @@ -430,7 +444,7 @@ export async function exportWorkspacesData( parentDoc: BaseModel | null, includePrivateDocs: boolean, format: 'json' | 'yaml', -): Promise { +) { const docs: Array = await getDocWithDescendants(parentDoc, includePrivateDocs); const requests: Array = docs.filter(doc => isRequest(doc) || isGrpcRequest(doc)); return exportRequestsData(requests, includePrivateDocs, format); @@ -440,8 +454,9 @@ export async function exportRequestsData( requests: Array, includePrivateDocs: boolean, format: 'json' | 'yaml', -): Promise { - const data = { +) { + const data: Insomnia4Data = { + // @ts-expect-error -- TSCONVERSION maybe this needs to be added to the upstream type? _type: 'export', __export_format: EXPORT_FORMAT, __export_date: new Date(), @@ -450,17 +465,21 @@ export async function exportRequestsData( }; const docs: Array = []; const workspaces: Array = []; - const mapTypeAndIdToDoc: Object = {}; + const mapTypeAndIdToDoc: Record = {}; for (const req of requests) { const ancestors: Array = clone(await db.withAncestors(req)); + for (const ancestor of ancestors) { const key = ancestor.type + '___' + ancestor._id; + if (mapTypeAndIdToDoc.hasOwnProperty(key)) { continue; } + mapTypeAndIdToDoc[key] = ancestor; docs.push(ancestor); + if (isWorkspace(ancestor)) { workspaces.push(ancestor); } @@ -496,47 +515,64 @@ export async function exportRequestsData( isProtoFile(d) || isProtoDirectory(d) || isWorkspace(d) || + // @ts-expect-error -- TSCONVERSION maybe this needs to be added to the upstream type? d.type === models.cookieJar.type || + // @ts-expect-error -- TSCONVERSION maybe this needs to be added to the upstream type? d.type === models.environment.type || + // @ts-expect-error -- TSCONVERSION maybe this needs to be added to the upstream type? d.type === models.apiSpec.type ) ) { return false; } + // BaseModel doesn't have isPrivate, so cast it first. - return !(d: Object).isPrivate || includePrivateDocs; + return !d.isPrivate || includePrivateDocs; }) - .map((d: Object) => { + .map(d => { if (isWorkspace(d)) { + // @ts-expect-error -- TSCONVERSION maybe this needs to be added to the upstream type? d._type = EXPORT_TYPE_WORKSPACE; } else if (d.type === models.cookieJar.type) { + // @ts-expect-error -- TSCONVERSION maybe this needs to be added to the upstream type? d._type = EXPORT_TYPE_COOKIE_JAR; } else if (d.type === models.environment.type) { + // @ts-expect-error -- TSCONVERSION maybe this needs to be added to the upstream type? d._type = EXPORT_TYPE_ENVIRONMENT; } else if (d.type === models.unitTestSuite.type) { + // @ts-expect-error -- TSCONVERSION maybe this needs to be added to the upstream type? d._type = EXPORT_TYPE_UNIT_TEST_SUITE; } else if (d.type === models.unitTest.type) { + // @ts-expect-error -- TSCONVERSION maybe this needs to be added to the upstream type? d._type = EXPORT_TYPE_UNIT_TEST; } else if (isRequestGroup(d)) { + // @ts-expect-error -- TSCONVERSION maybe this needs to be added to the upstream type? d._type = EXPORT_TYPE_REQUEST_GROUP; } else if (isRequest(d)) { + // @ts-expect-error -- TSCONVERSION maybe this needs to be added to the upstream type? d._type = EXPORT_TYPE_REQUEST; } else if (isGrpcRequest(d)) { + // @ts-expect-error -- TSCONVERSION maybe this needs to be added to the upstream type? d._type = EXPORT_TYPE_GRPC_REQUEST; } else if (isProtoFile(d)) { + // @ts-expect-error -- TSCONVERSION maybe this needs to be added to the upstream type? d._type = EXPORT_TYPE_PROTO_FILE; } else if (isProtoDirectory(d)) { + // @ts-expect-error -- TSCONVERSION maybe this needs to be added to the upstream type? d._type = EXPORT_TYPE_PROTO_DIRECTORY; + // @ts-expect-error -- TSCONVERSION maybe this needs to be added to the upstream type? } else if (d.type === models.apiSpec.type) { + // @ts-expect-error -- TSCONVERSION maybe this needs to be added to the upstream type? d._type = EXPORT_TYPE_API_SPEC; } + // @ts-expect-error -- TSCONVERSION maybe this needs to be added to the upstream type? // Delete the things we don't want to export delete d.type; return d; }); - trackEvent('Data', 'Export', `Insomnia ${format}`); + if (format.toLowerCase() === 'yaml') { return YAML.stringify(data); } else if (format.toLowerCase() === 'json') { @@ -548,12 +584,11 @@ export async function exportRequestsData( async function getDocWithDescendants( parentDoc: BaseModel | null = null, - includePrivateDocs: boolean = false, + includePrivateDocs = false, ): Promise> { const docs = await db.withDescendants(parentDoc); return docs.filter( - d => - // Don't include if private, except if we want to - !(d: any).isPrivate || includePrivateDocs, + // Don't include if private, except if we want to + doc => !doc?.isPrivate || includePrivateDocs, ); } diff --git a/packages/insomnia-app/app/common/keyboard-keys.js b/packages/insomnia-app/app/common/keyboard-keys.ts similarity index 99% rename from packages/insomnia-app/app/common/keyboard-keys.js rename to packages/insomnia-app/app/common/keyboard-keys.ts index 889bf36a2c..8ff3e9f9e8 100644 --- a/packages/insomnia-app/app/common/keyboard-keys.js +++ b/packages/insomnia-app/app/common/keyboard-keys.ts @@ -1,6 +1,4 @@ -// @flow - -export const keyboardKeys: Object = { +export const keyboardKeys: Record = { backspace: { keyCode: 8, label: 'Backspace', diff --git a/packages/insomnia-app/app/common/log.js b/packages/insomnia-app/app/common/log.ts similarity index 82% rename from packages/insomnia-app/app/common/log.js rename to packages/insomnia-app/app/common/log.ts index 0fe1806335..a676101f6d 100644 --- a/packages/insomnia-app/app/common/log.js +++ b/packages/insomnia-app/app/common/log.ts @@ -1,4 +1,3 @@ -// @flow import log from 'electron-log'; import { isDevelopment } from './constants'; import { dirname } from 'path'; @@ -13,8 +12,8 @@ export const initializeLogging = () => { // Set the max log file size to 10mb // When the log file exceeds this limit, it will be rotated to {file name}.old.log file. fileTransport.maxSize = 1024 * 1024 * 10; - // Rotate the log file every time we start the app + // @ts-expect-error -- TSCONVERSION seems like something is wrong here but I don't want to convert to string until I can take a closer look fileTransport.archiveLog(logFile); logFile.clear(); } @@ -23,7 +22,7 @@ export const initializeLogging = () => { Object.assign(console, log.functions); }; -export function getLogDirectory(): string { +export function getLogDirectory() { const logPath = log.transports.file.getFile().path; return dirname(logPath); } diff --git a/packages/insomnia-app/app/common/markdown-to-html.js b/packages/insomnia-app/app/common/markdown-to-html.ts similarity index 81% rename from packages/insomnia-app/app/common/markdown-to-html.js rename to packages/insomnia-app/app/common/markdown-to-html.ts index 73c90f5784..cacb236c47 100644 --- a/packages/insomnia-app/app/common/markdown-to-html.js +++ b/packages/insomnia-app/app/common/markdown-to-html.ts @@ -3,6 +3,7 @@ import marked from 'marked'; marked.setOptions({ renderer: new marked.Renderer(), gfm: true, + // @ts-expect-error -- TSCONVERSION missing from marked types tables: true, breaks: false, pedantic: false, diff --git a/packages/insomnia-app/app/common/migrate-from-designer.js b/packages/insomnia-app/app/common/migrate-from-designer.ts similarity index 81% rename from packages/insomnia-app/app/common/migrate-from-designer.js rename to packages/insomnia-app/app/common/migrate-from-designer.ts index 1717ed542d..7139f794f8 100644 --- a/packages/insomnia-app/app/common/migrate-from-designer.js +++ b/packages/insomnia-app/app/common/migrate-from-designer.ts @@ -1,10 +1,9 @@ -// @flow import NeDB from 'nedb'; import type { BaseModel } from '../models'; import fsPath from 'path'; import fs from 'fs'; import * as models from '../models'; -import * as db from './database'; +import { database as db } from './database'; import { getModelName } from '../models'; import { difference } from 'lodash'; import type { Workspace } from '../models/workspace'; @@ -14,17 +13,19 @@ import * as electron from 'electron'; import { trackEvent } from './analytics'; import { WorkspaceScopeKeys } from '../models/workspace'; -async function loadDesignerDb(types: Array, designerDataDir: string): Promise { +async function loadDesignerDb( + types: Array, + designerDataDir: string, +): Promise> { const designerDb = {}; - types.forEach(type => { designerDb[type] = []; // initialize each type to empty array }); - const promises = types.map( type => - new Promise((resolve, reject) => { + new Promise((resolve, reject) => { const filePath = fsPath.join(designerDataDir, `insomnia.${type}.db`); + if (!fs.existsSync(filePath)) { console.log(`[db] db file for ${type} not found: ${filePath}`); resolve(); @@ -36,76 +37,64 @@ async function loadDesignerDb(types: Array, designerDataDir: string): Pr filename: filePath, corruptAlertThreshold: 0.9, }); - // Find every entry and store in memory collection.find({}, (err, docs: Array) => { if (err) { return reject(err); } - (designerDb[type]: Array).push(...docs); + (designerDb[type] as Array>).push(...docs); resolve(); }); }), ); - await Promise.all(promises); - // Return entries, but no longer linked to the database files return designerDb; } -type DBType = { [string]: Array }; +type DBType = Record>; -export type MigrationOptions = { - useDesignerSettings: boolean, - copyPlugins: boolean, - copyWorkspaces: boolean, - designerDataDir: string, - coreDataDir: string, -}; +export interface MigrationOptions { + useDesignerSettings: boolean; + copyPlugins: boolean; + copyWorkspaces: boolean; + designerDataDir: string; + coreDataDir: string; +} -export type MigrationResult = { - error?: Error, -}; +export interface MigrationResult { + error?: Error; +} async function createCoreBackup(modelTypes: Array, coreDataDir: string) { - console.log(`[db-merge] creating backup`); - + console.log('[db-merge] creating backup'); const backupDir = fsPath.join(coreDataDir, 'core-backup'); await fsx.remove(backupDir); await fsx.ensureDir(backupDir); - // Copy db files const filesToCopy = modelTypes.map(modelType => `insomnia.${modelType}.db`); for (const entryName of filesToCopy) { const src = fsPath.join(coreDataDir, entryName); const dest = fsPath.join(backupDir, entryName); - await fsx.copy(src, dest); } // Copy dirs const dirsToCopy = ['plugins', 'responses', 'version-control']; - await copyDirs(dirsToCopy, coreDataDir, backupDir); - console.log(`[db-merge] backup created at ${backupDir}`); - return backupDir; } async function migratePlugins(designerDataDir: string, coreDataDir: string) { const designerPluginDir = fsPath.join(designerDataDir, 'plugins'); const corePluginDir = fsPath.join(coreDataDir, 'plugins'); - // get list of plugins in Designer const designerPlugins = await readDirs(designerPluginDir); - await removeDirs(designerPlugins, corePluginDir); await copyDirs(designerPlugins, designerPluginDir, corePluginDir); - // Remove plugin bundle from installed plugins because it's included with the app now const pluginsToDelete = [ 'insomnia-plugin-kong-bundle', @@ -116,7 +105,7 @@ async function migratePlugins(designerDataDir: string, coreDataDir: string) { await removeDirs(pluginsToDelete, corePluginDir); } -async function readDirs(srcDir: string): Array { +async function readDirs(srcDir: string) { if (existsAndIsDirectory(srcDir)) { return await fs.promises.readdir(srcDir); } else { @@ -140,47 +129,46 @@ async function copyDirs(dirs: Array, srcDir: string, destDir: string) { async function removeDirs(dirs: Array, srcDir: string) { for (const dir of dirs.filter(c => c)) { const dirToRemove = fsPath.join(srcDir, dir); + if (existsAndIsDirectory(dirToRemove)) { await fsx.remove(dirToRemove); } } } -export function existsAndIsDirectory(name: string): boolean { +export function existsAndIsDirectory(name: string) { return fs.existsSync(name) && fs.statSync(name).isDirectory(); } - export default async function migrateFromDesigner({ useDesignerSettings, designerDataDir, coreDataDir, copyPlugins, copyWorkspaces, -}: MigrationOptions): Promise { +}: MigrationOptions) { console.log( `[db-merge] starting process for migrating from ${designerDataDir} to ${coreDataDir}`, ); - const nonWorkspaceModels = [ models.stats.type, // TODO: investigate further any implications that may invalidate collected stats models.settings.type, ]; - // Every model except those to ignore and settings is a "workspace" model const workspaceModels = difference(models.types(), nonWorkspaceModels); - const modelTypesToMerge = []; if (useDesignerSettings) { trackEvent('Data', 'Migration', 'Settings'); + // @ts-expect-error -- TSCONVERSION modelTypesToMerge.push(models.settings.type); - console.log(`[db-merge] keeping settings from Insomnia Designer`); + console.log('[db-merge] keeping settings from Insomnia Designer'); } else { - console.log(`[db-merge] keeping settings from Insomnia Core`); + console.log('[db-merge] keeping settings from Insomnia Core'); } if (copyWorkspaces) { trackEvent('Data', 'Migration', 'Workspaces'); + // @ts-expect-error -- TSCONVERSION modelTypesToMerge.push(...workspaceModels); } @@ -189,7 +177,6 @@ export default async function migrateFromDesigner({ try { // Create core backup backupDir = await createCoreBackup(modelTypesToMerge, coreDataDir); - // Load designer database const designerDb: DBType = await loadDesignerDb(modelTypesToMerge, designerDataDir); @@ -207,7 +194,7 @@ export default async function migrateFromDesigner({ ]; propertiesToPersist.forEach(s => { if (coreSettings.hasOwnProperty(s)) { - (entries[0]: Settings)[s] = coreSettings[s]; + (entries[0] as Settings)[s] = coreSettings[s]; } }); } @@ -215,7 +202,7 @@ export default async function migrateFromDesigner({ // For each workspace coming from Designer, mark workspace.scope as 'design' if (modelType === models.workspace.type) { for (const workspace of entries) { - (workspace: Workspace).scope = WorkspaceScopeKeys.design; + (workspace as Workspace).scope = WorkspaceScopeKeys.design; } } @@ -223,25 +210,26 @@ export default async function migrateFromDesigner({ console.log( `[db-merge] merging ${entryCount} ${getModelName(modelType, entryCount)} from Designer`, ); - await db.batchModifyDocs({ upsert: entries, remove: [] }); + await db.batchModifyDocs({ + upsert: entries, + remove: [], + }); } if (copyWorkspaces) { - console.log(`[db-merge] migrating version control data from designer to core`); + console.log('[db-merge] migrating version control data from designer to core'); await copyDirs(['version-control'], designerDataDir, coreDataDir); - - console.log(`[db-merge] migrating response cache from designer to core`); + console.log('[db-merge] migrating response cache from designer to core'); await copyDirs(['responses'], designerDataDir, coreDataDir); } if (copyPlugins) { - console.log(`[db-merge] migrating plugins from designer to core`); + console.log('[db-merge] migrating plugins from designer to core'); trackEvent('Data', 'Migration', 'Plugins'); await migratePlugins(designerDataDir, coreDataDir); } console.log('[db-merge] done!'); - trackEvent('Data', 'Migration', 'Success'); return {}; } catch (error) { @@ -249,13 +237,14 @@ export default async function migrateFromDesigner({ console.error(error); trackEvent('Data', 'Migration', 'Failure'); await restoreCoreBackup(backupDir, coreDataDir); - return { error }; + return { + error, + } as MigrationResult; } } - export async function restoreCoreBackup(backupDir: string, coreDataDir: string) { if (!backupDir) { - console.log(`[db-merge] nothing to restore; no backup was created`); + console.log('[db-merge] nothing to restore; no backup was created'); return; } @@ -264,12 +253,11 @@ export async function restoreCoreBackup(backupDir: string, coreDataDir: string) return; } - console.log(`[db-merge] restoring from backup`); + console.log('[db-merge] restoring from backup'); await removeDirs(['plugins', 'responses', 'version-control'], coreDataDir); await fsx.copy(backupDir, coreDataDir); - console.log(`[db-merge] restored from backup`); + console.log('[db-merge] restored from backup'); } - export function restartApp() { const { app } = electron.remote || electron; app.relaunch(); diff --git a/packages/insomnia-app/app/common/misc.js b/packages/insomnia-app/app/common/misc.ts similarity index 62% rename from packages/insomnia-app/app/common/misc.js rename to packages/insomnia-app/app/common/misc.ts index 78e59ed068..38cccc7d80 100644 --- a/packages/insomnia-app/app/common/misc.js +++ b/packages/insomnia-app/app/common/misc.ts @@ -1,5 +1,3 @@ -// @flow -import * as electron from 'electron'; import { Readable, Writable } from 'stream'; import fuzzysort from 'fuzzysort'; import * as uuid from 'uuid'; @@ -9,17 +7,20 @@ import { METHOD_OPTIONS, METHOD_DELETE, DEBOUNCE_MILLIS } from './constants'; const ESCAPE_REGEX_MATCH = /[-[\]/{}()*+?.\\^$|]/g; -type Header = { - name: string, - value: string, -}; +interface Header { + name: string; + value: string; +} -type Parameter = { - name: string, - value: string, -}; +interface Parameter { + name: string; + value: string; +} -export function filterParameters(parameters: Array, name: string): Array { +export function filterParameters( + parameters: Array, + name: string, +): Array { if (!Array.isArray(parameters) || !name) { return []; } @@ -27,75 +28,75 @@ export function filterParameters(parameters: Array, name: strin return parameters.filter(h => (!h || !h.name ? false : h.name === name)); } -export function filterHeaders(headers: Array, name: string): Array { - if (!Array.isArray(headers) || !name || !(typeof name === 'string')) { +export function filterHeaders(headers: Array, name?: string): Array { + if (!Array.isArray(headers) || !name || typeof name !== 'string') { return []; } - return headers.filter(h => { + return headers.filter(header => { // Never match against invalid headers - if (!h || !h.name || typeof h.name !== 'string') { + if (!header || !header.name || typeof header.name !== 'string') { return false; } - return h.name.toLowerCase() === name.toLowerCase(); + return header.name.toLowerCase() === name.toLowerCase(); }); } -export function hasContentTypeHeader(headers: Array): boolean { +export function hasContentTypeHeader(headers: Array) { return filterHeaders(headers, 'content-type').length > 0; } -export function hasContentLengthHeader(headers: Array): boolean { +export function hasContentLengthHeader(headers: Array) { return filterHeaders(headers, 'content-length').length > 0; } -export function hasAuthHeader(headers: Array): boolean { +export function hasAuthHeader(headers: Array) { return filterHeaders(headers, 'authorization').length > 0; } -export function hasAcceptHeader(headers: Array): boolean { +export function hasAcceptHeader(headers: Array) { return filterHeaders(headers, 'accept').length > 0; } -export function hasUserAgentHeader(headers: Array): boolean { +export function hasUserAgentHeader(headers: Array) { return filterHeaders(headers, 'user-agent').length > 0; } -export function hasAcceptEncodingHeader(headers: Array): boolean { +export function hasAcceptEncodingHeader(headers: Array) { return filterHeaders(headers, 'accept-encoding').length > 0; } -export function getSetCookieHeaders(headers: Array): Array { +export function getSetCookieHeaders(headers: Array): Array { return filterHeaders(headers, 'set-cookie'); } -export function getLocationHeader(headers: Array): T | null { +export function getLocationHeader(headers: Array): T | null { const matches = filterHeaders(headers, 'location'); return matches.length ? matches[0] : null; } -export function getContentTypeHeader(headers: Array): T | null { +export function getContentTypeHeader(headers: Array): T | null { const matches = filterHeaders(headers, 'content-type'); return matches.length ? matches[0] : null; } -export function getMethodOverrideHeader(headers: Array): T | null { +export function getMethodOverrideHeader(headers: Array): T | null { const matches = filterHeaders(headers, 'x-http-method-override'); return matches.length ? matches[0] : null; } -export function getHostHeader(headers: Array): T | null { +export function getHostHeader(headers: Array): T | null { const matches = filterHeaders(headers, 'host'); return matches.length ? matches[0] : null; } -export function getContentDispositionHeader(headers: Array): T | null { +export function getContentDispositionHeader(headers: Array): T | null { const matches = filterHeaders(headers, 'content-disposition'); return matches.length ? matches[0] : null; } -export function getContentLengthHeader(headers: Array): T | null { +export function getContentLengthHeader(headers: Array): T | null { const matches = filterHeaders(headers, 'content-length'); return matches.length ? matches[0] : null; } @@ -105,7 +106,7 @@ export function getContentLengthHeader(headers: Array): T | null { * @param prefix * @returns {string} */ -export function generateId(prefix: string): string { +export function generateId(prefix?: string) { const id = uuid.v4().replace(/-/g, ''); if (prefix) { @@ -115,31 +116,32 @@ export function generateId(prefix: string): string { } } -export function delay(milliseconds: number = DEBOUNCE_MILLIS): Promise { - return new Promise(resolve => setTimeout(resolve, milliseconds)); +export function delay(milliseconds: number = DEBOUNCE_MILLIS) { + return new Promise(resolve => setTimeout(resolve, milliseconds)); } -export function removeVowels(str: string): string { +export function removeVowels(str: string) { return str.replace(/[aeiouyAEIOUY]/g, ''); } -export function formatMethodName(method: string): string { +export function formatMethodName(method: string) { let methodName = method || ''; + if (method === METHOD_DELETE || method === METHOD_OPTIONS) { methodName = method.slice(0, 3); } else if (method.length > 4) { methodName = removeVowels(method).slice(0, 4); } + return methodName; } -export function keyedDebounce(callback: Function, millis: number = DEBOUNCE_MILLIS): Function { +export function keyedDebounce(callback: T, millis: number = DEBOUNCE_MILLIS): T { let timeout; let results = {}; - - return function(key, ...args) { + // @ts-expect-error -- TSCONVERSION + const t: T = function(key, ...args) { results[key] = args; - clearTimeout(timeout); timeout = setTimeout(() => { if (!Object.keys(results).length) { @@ -150,23 +152,27 @@ export function keyedDebounce(callback: Function, millis: number = DEBOUNCE_MILL results = {}; }, millis); }; + return t; } -export function debounce(callback: Function, millis: number = DEBOUNCE_MILLIS): Function { +export function debounce( + callback: T, + milliseconds: number = DEBOUNCE_MILLIS, +): T { // For regular debounce, just use a keyed debounce with a fixed key return keyedDebounce(results => { + // eslint-disable-next-line prefer-spread -- don't know if there was a "this binding" reason for this being this way so I'm leaving it alone callback.apply(null, results.__key__); - }, millis).bind(null, '__key__'); + }, milliseconds).bind(null, '__key__'); } -export function describeByteSize(bytes: number, long: boolean = false): string { +export function describeByteSize(bytes: number, long = false) { bytes = Math.round(bytes * 10) / 10; let size; - // NOTE: We multiply these by 2 so we don't end up with // values like 0 GB - let unit; + if (bytes < 1024 * 2) { size = bytes; unit = long ? 'bytes' : 'B'; @@ -185,19 +191,15 @@ export function describeByteSize(bytes: number, long: boolean = false): string { return `${rounded} ${unit}`; } -export function nullFn(): void { +export function nullFn() { // Do nothing } -export function preventDefault(e: Event): void { +export function preventDefault(e: Event) { e.preventDefault(); } -export function clickLink(href: string): void { - electron.shell.openExternal(href); -} - -export function fnOrString(v: string | Function, ...args: Array) { +export function fnOrString(v: string | ((...args: Array) => any), ...args: Array) { if (typeof v === 'string') { return v; } else { @@ -205,7 +207,7 @@ export function fnOrString(v: string | Function, ...args: Array) { } } -export function compressObject(obj: any): string { +export function compressObject(obj: any) { const compressed = zlib.gzipSync(JSON.stringify(obj)); return compressed.toString('base64'); } @@ -219,7 +221,7 @@ export function decompressObject(input: string | null): any { return JSON.parse(jsonBuffer.toString('utf8')); } -export function resolveHomePath(p: string): string { +export function resolveHomePath(p: string) { if (p.indexOf('~/') === 0) { return pathJoin(process.env.HOME || '/', p.slice(1)); } else { @@ -235,7 +237,7 @@ export function jsonParseOr(str: string, fallback: any): any { } } -export function escapeHTML(unsafeText: string): string { +export function escapeHTML(unsafeText: string) { const div = document.createElement('div'); div.innerText = unsafeText; return div.innerHTML; @@ -246,37 +248,48 @@ export function escapeHTML(unsafeText: string): string { * @param str - string to escape * @returns {string} escaped string */ -export function escapeRegex(str: string): string { +export function escapeRegex(str: string) { return str.replace(ESCAPE_REGEX_MATCH, '\\$&'); } export function fuzzyMatch( searchString: string, text: string, - options: { splitSpace?: boolean, loose?: boolean } = {}, -): null | { score: number, indexes: Array } { + options: { + splitSpace?: boolean; + loose?: boolean; + } = {}, +): null | { + score: number; + indexes: Array; +} { return fuzzyMatchAll(searchString, [text], options); } export function fuzzyMatchAll( searchString: string, allText: Array, - options: { splitSpace?: boolean, loose?: boolean } = {}, -): null | { score: number, indexes: Array } { + options: { + splitSpace?: boolean; + loose?: boolean; + } = {}, +) { if (!searchString || !searchString.trim()) { return null; } const words = searchString.split(' ').filter(w => w.trim()); const terms = options.splitSpace ? [...words, searchString] : [searchString]; - - let maxScore = null; - let indexes = []; + let maxScore: number | null = null; + let indexes: Array = []; let termsMatched = 0; + for (const term of terms) { let matchedTerm = false; + for (const text of allText.filter(t => !t || t.trim())) { const result = fuzzysort.single(term, text); + if (!result) { continue; } @@ -315,62 +328,30 @@ export function fuzzyMatchAll( }; } -export function getViewportSize(): string | null { - const { BrowserWindow } = electron.remote || electron; - const w = BrowserWindow.getFocusedWindow() || BrowserWindow.getAllWindows()[0]; - - if (w) { - const { width, height } = w.getContentBounds(); - return `${width}x${height}`; - } else { - // No windows open - return null; - } -} - -export function getScreenResolution(): string { - const { screen } = electron.remote || electron; - const { width, height } = screen.getPrimaryDisplay().workAreaSize; - return `${width}x${height}`; -} - -export function getUserLanguage(): string { - const { app } = electron.remote || electron; - return app.getLocale(); -} - -export async function waitForStreamToFinish(s: Readable | Writable): Promise { - return new Promise(resolve => { - if ((s: any)._readableState && (s: any)._readableState.finished) { +export async function waitForStreamToFinish(stream: Readable | Writable) { + return new Promise(resolve => { + // @ts-expect-error -- access of internal values that are intended to be private. We should _not_ do this. + if (stream._readableState?.finished) { return resolve(); } - if ((s: any)._writableState && (s: any)._writableState.finished) { + // @ts-expect-error -- access of internal values that are intended to be private. We should _not_ do this. + if (stream._writableState?.finished) { return resolve(); } - s.on('close', () => { + stream.on('close', () => { resolve(); }); - - s.on('error', () => { + stream.on('error', () => { resolve(); }); }); } -export function getDesignerDataDir(): string { - const { app } = electron.remote || electron; - return process.env.DESIGNER_DATA_PATH || pathJoin(app.getPath('appData'), 'Insomnia Designer'); -} +export function chunkArray(arr: Array, chunkSize: number) { + const chunks: Array> = []; -export function getDataDirectory(): string { - const { app } = electron.remote || electron; - return process.env.INSOMNIA_DATA_PATH || app.getPath('userData'); -} - -export function chunkArray(arr: Array, chunkSize: number): Array> { - const chunks = []; for (let i = 0, j = arr.length; i < j; i += chunkSize) { chunks.push(arr.slice(i, i + chunkSize)); } @@ -378,7 +359,7 @@ export function chunkArray(arr: Array, chunkSize: number): Array> return chunks; } -export function pluralize(text: string): string { +export function pluralize(text: string) { let trailer = 's'; let chop = 0; @@ -398,7 +379,7 @@ export function pluralize(text: string): string { return `${text.slice(0, text.length - chop)}${trailer}`; } -export function diffPatchObj(baseObj: {}, patchObj: {}, deep = false): ObjectComparison { +export function diffPatchObj(baseObj: {}, patchObj: {}, deep = false) { const clonedBaseObj = JSON.parse(JSON.stringify(baseObj)); for (const prop in baseObj) { @@ -437,21 +418,20 @@ export function diffPatchObj(baseObj: {}, patchObj: {}, deep = false): ObjectCom return clonedBaseObj; } -export function isObject(obj: any) { +export function isObject(obj: unknown) { return obj !== null && typeof obj === 'object'; } +/** + Finds epoch's digit count and converts it to make it exactly 13 digits. + Which is the epoch millisecond represntation. +*/ export function convertEpochToMilliseconds(epoch: number) { - /* - Finds epoch's digit count and converts it to make it exactly 13 digits. - Which is the epoch millisecond represntation. - */ const expDigitCount = epoch.toString().length; - const convertedEpoch = parseInt(epoch * 10 ** (13 - expDigitCount)); - return convertedEpoch; + return parseInt(String(epoch * 10 ** (13 - expDigitCount)), 10); } -export function snapNumberToLimits(value: number, min?: number, max?: number): number { +export function snapNumberToLimits(value: number, min?: number, max?: number) { const moreThanMax = max && !Number.isNaN(max) && value > max; const lessThanMin = min && !Number.isNaN(min) && value < min; @@ -464,6 +444,6 @@ export function snapNumberToLimits(value: number, min?: number, max?: number): n return value; } -export function isNotNullOrUndefined(obj: any): boolean { +export function isNotNullOrUndefined(obj: unknown) { return obj !== null && obj !== undefined; } diff --git a/packages/insomnia-app/app/common/render.js b/packages/insomnia-app/app/common/render.ts similarity index 91% rename from packages/insomnia-app/app/common/render.js rename to packages/insomnia-app/app/common/render.ts index 47d5d93078..787ae62077 100644 --- a/packages/insomnia-app/app/common/render.js +++ b/packages/insomnia-app/app/common/render.ts @@ -1,12 +1,10 @@ -// @flow import type { Request } from '../models/request'; import type { BaseModel } from '../models/index'; - import { setDefaultProtocol } from 'insomnia-url'; import clone from 'clone'; import * as models from '../models'; import { CONTENT_TYPE_GRAPHQL, JSON_ORDER_SEPARATOR } from './constants'; -import * as db from './database'; +import { database as db } from './database'; import * as templating from '../templating'; import type { CookieJar } from '../models/cookie-jar'; import type { Environment } from '../models/environment'; @@ -17,31 +15,49 @@ import { isRequestGroup } from '../models/helpers/is-model'; export const KEEP_ON_ERROR = 'keep'; export const THROW_ON_ERROR = 'throw'; - export type RenderPurpose = 'send' | 'general' | 'no-render'; - export const RENDER_PURPOSE_SEND: RenderPurpose = 'send'; export const RENDER_PURPOSE_GENERAL: RenderPurpose = 'general'; export const RENDER_PURPOSE_NO_RENDER: RenderPurpose = 'no-render'; /** Key/value pairs to be provided to the render context */ -export type ExtraRenderInfo = Array<{ name: string, value: any }>; +export type ExtraRenderInfo = Array<{ + name: string; + value: any; +}>; export type RenderedRequest = Request & { - cookies: Array<{ name: string, value: string, disabled?: boolean }>, - cookieJar: CookieJar, + cookies: Array<{ + name: string; + value: string; + disabled?: boolean; + }>; + cookieJar: CookieJar; }; export type RenderedGrpcRequest = GrpcRequest; + export type RenderedGrpcRequestBody = GrpcRequestBody; +export interface RenderContextAndKeys { + context: Record; + keys: Array<{ + name: string; + value: any; + }> +} + +export type HandleGetRenderContext = () => Promise; + +export type HandleRender = (object: T, contextCacheKey?: string | null) => Promise; + export async function buildRenderContext( ancestors: Array | null, rootEnvironment: Environment | null, subEnvironment: Environment | null, - baseContext: Object = {}, -): Object { - const envObjects = []; + baseContext: Record = {}, +) { + const envObjects: Array> = []; // Get root environment keys in correct order // Then get sub environment keys in correct order @@ -52,7 +68,6 @@ export async function buildRenderContext( rootEnvironment.dataPropertyOrder, JSON_ORDER_SEPARATOR, ); - envObjects.push(ordered); } @@ -62,13 +77,13 @@ export async function buildRenderContext( subEnvironment.dataPropertyOrder, JSON_ORDER_SEPARATOR, ); - envObjects.push(ordered); } for (const doc of (ancestors || []).reverse()) { const ancestor: any = doc; const { environment, environmentPropertyOrder } = ancestor; + if (typeof environment === 'object' && environment !== null) { const ordered = orderedJSON.order( environment, @@ -87,8 +102,12 @@ export async function buildRenderContext( let renderContext = baseContext; // Made the rendering into a recursive function to handle nested Objects - async function renderSubContext(subObject: Object, subContext: Object): Promise { + async function renderSubContext( + subObject: Record, + subContext: Record, + ) { const keys = _getOrderedEnvironmentKeys(subObject); + for (const key of keys) { /* * If we're overwriting a string, try to render it first using the same key from the base @@ -126,10 +145,11 @@ export async function buildRenderContext( subContext[key] = subObject[key]; } } + return subContext; } - for (const envObject: Object of envObjects) { + for (const envObject of envObjects) { // For every environment render the Objects renderContext = await renderSubContext(envObject, renderContext); } @@ -141,6 +161,7 @@ export async function buildRenderContext( // Render recursive references and tags. const skipNextTime = {}; + for (let i = 0; i < 3; i++) { for (const key of keys) { // Skip rendering keys that stayed the same multiple times. This is here because @@ -168,6 +189,7 @@ export async function buildRenderContext( finalRenderContext[key] = renderResult; } } + return finalRenderContext; } @@ -182,15 +204,15 @@ export async function buildRenderContext( */ export async function render( obj: T, - context: Object = {}, + context: Record = {}, blacklistPathRegex: RegExp | null = null, errorMode: string = THROW_ON_ERROR, - name: string = '', -): Promise { + name = '', +) { // Make a deep copy so no one gets mad :) const newObj = clone(obj); - async function next(x: any, path: string, first: boolean = false): Promise { + async function next(x: T, path: string, first = false) { if (blacklistPathRegex && path.match(blacklistPathRegex)) { return x; } @@ -210,12 +232,15 @@ export async function render( // Do nothing to these types } else if (typeof x === 'string') { try { + // @ts-expect-error -- TSCONVERSION x = await templating.render(x, { context, path }); // If the variable outputs a tag, render it again. This is a common use // case for environment variables: // {{ foo }} => {% uuid 'v4' %} => dd265685-16a3-4d76-a59c-e8264c16835a + // @ts-expect-error -- TSCONVERSION if (x.includes('{%')) { + // @ts-expect-error -- TSCONVERSION x = await templating.render(x, { context, path }); } } catch (err) { @@ -230,11 +255,13 @@ export async function render( } else if (typeof x === 'object' && x !== null) { // Don't even try rendering disabled objects // Note, this logic probably shouldn't be here, but w/e for now + // @ts-expect-error -- TSCONVERSION if (x.disabled) { return x; } const keys = Object.keys(x); + for (const key of keys) { if (first && key.indexOf('_') === 0) { x[key] = await next(x[key], path); @@ -248,21 +275,21 @@ export async function render( return x; } - return next(newObj, name, true); + return next(newObj, name, true); } - export async function getRenderContext( - request: Request | GrpcRequest, + request: Request | GrpcRequest | null, environmentId: string | null, ancestors: Array | null = null, purpose: RenderPurpose | null = null, extraInfo: ExtraRenderInfo | null = null, -): Promise { +): Promise> { if (!ancestors) { ancestors = await _getRequestAncestors(request); } const workspace = ancestors.find(doc => doc.type === models.workspace.type); + if (!workspace) { throw new Error('Failed to render. Could not find workspace'); } @@ -271,7 +298,6 @@ export async function getRenderContext( workspace ? workspace._id : 'n/a', ); const subEnvironment = await models.environment.getById(environmentId || 'n/a'); - const keySource = {}; // Function that gets Keys and stores their Source location @@ -283,6 +309,7 @@ export async function getRenderContext( // Recurse down for Objects and Arrays const typeStr = Object.prototype.toString.call(subObject); + if (typeStr === '[object Object]') { for (const key of Object.keys(subObject)) { getKeySource(subObject[key], templatingUtils.forceBracketNotation(inKey, key), inSource); @@ -295,7 +322,6 @@ export async function getRenderContext( } const inKey = templating.NUNJUCKS_TEMPLATE_GLOBAL_PROPERTY_NAME; - // Get Keys from root environment getKeySource((rootEnvironment || {}).data, inKey, 'root'); @@ -308,6 +334,7 @@ export async function getRenderContext( if (ancestors) { for (let idx = 0; idx < ancestors.length; idx++) { const ancestor: any = ancestors[idx] || {}; + if ( isRequestGroup(ancestor) && ancestor.hasOwnProperty('environment') && @@ -319,7 +346,8 @@ export async function getRenderContext( } // Add meta data helper function - const baseContext = {}; + const baseContext: Record = {}; + baseContext.getMeta = () => ({ requestId: request ? request._id : null, workspaceId: workspace ? workspace._id : 'n/a', @@ -330,6 +358,7 @@ export async function getRenderContext( }); baseContext.getPurpose = () => purpose; + baseContext.getExtraInfo = (key: string) => { if (!Array.isArray(extraInfo)) { return null; @@ -344,14 +373,13 @@ export async function getRenderContext( // Generate the context we need to render return buildRenderContext(ancestors, rootEnvironment, subEnvironment, baseContext); } - export async function getRenderedGrpcRequest( request: GrpcRequest, environmentId: string | null, purpose?: RenderPurpose, extraInfo?: ExtraRenderInfo, skipBody?: boolean, -): Promise<{ request: RenderedGrpcRequest, context: Object }> { +) { const renderContext = await getRenderContext( request, environmentId, @@ -359,24 +387,18 @@ export async function getRenderedGrpcRequest( purpose, extraInfo || null, ); - const description = request.description; - // Render description separately because it's lower priority request.description = ''; - // Ignore body by default and only include if specified to const ignorePathRegex = skipBody ? /^body.*/ : null; - // Render all request properties const renderedRequest: RenderedGrpcRequest = await render( request, renderContext, ignorePathRegex, ); - renderedRequest.description = await render(description, renderContext, null, KEEP_ON_ERROR); - return renderedRequest; } @@ -385,7 +407,7 @@ export async function getRenderedGrpcRequestMessage( environmentId: string | null, purpose?: RenderPurpose, extraInfo?: ExtraRenderInfo, -): Promise { +) { const renderContext = await getRenderContext( request, environmentId, @@ -393,24 +415,20 @@ export async function getRenderedGrpcRequestMessage( purpose, extraInfo || null, ); - // Render request body const renderedBody: RenderedGrpcRequestBody = await render(request.body, renderContext); - return renderedBody; } - export async function getRenderedRequestAndContext( request: Request, environmentId: string | null, purpose?: RenderPurpose, extraInfo?: ExtraRenderInfo, -): Promise<{ request: RenderedRequest, context: Object }> { +) { const ancestors = await _getRequestAncestors(request); const workspace = ancestors.find(doc => doc.type === models.workspace.type); const parentId = workspace ? workspace._id : 'n/a'; const cookieJar = await models.cookieJar.getOrCreateForParentId(parentId); - const renderContext = await getRenderContext( request, environmentId, @@ -427,26 +445,25 @@ export async function getRenderedRequestAndContext( o.query = o.query.replace(/#}/g, '# }'); request.body.text = JSON.stringify(o); } - } catch (err) {} + } catch (err) { } // Render description separately because it's lower priority const description = request.description; request.description = ''; - // Render all request properties const renderResult = await render( - { _request: request, _cookieJar: cookieJar }, + { + _request: request, + _cookieJar: cookieJar, + }, renderContext, request.settingDisableRenderRequestBody ? /^body.*/ : null, ); - const renderedRequest = renderResult._request; const renderedCookieJar = renderResult._cookieJar; renderedRequest.description = await render(description, renderContext, null, KEEP_ON_ERROR); - // Remove disabled params renderedRequest.parameters = renderedRequest.parameters.filter(p => !p.disabled); - // Remove disabled headers renderedRequest.headers = renderedRequest.headers.filter(p => !p.disabled); @@ -462,7 +479,6 @@ export async function getRenderedRequestAndContext( // Default the proto if it doesn't exist renderedRequest.url = setDefaultProtocol(renderedRequest.url); - return { context: renderContext, request: { @@ -470,7 +486,6 @@ export async function getRenderedRequestAndContext( cookieJar: renderedCookieJar, cookies: [], isPrivate: false, - // NOTE: Flow doesn't like Object.assign, so we have to do each property manually // for now to convert Request to RenderedRequest. _id: renderedRequest._id, @@ -509,15 +524,17 @@ function _nunjucksSortValue(v) { return v && v.match && v.match(/({{|{%)/) ? 2 : 1; } -function _getOrderedEnvironmentKeys(finalRenderContext: Object): Array { +function _getOrderedEnvironmentKeys(finalRenderContext: Record): Array { return Object.keys(finalRenderContext).sort((k1, k2) => { const k1Sort = _nunjucksSortValue(finalRenderContext[k1]); + const k2Sort = _nunjucksSortValue(finalRenderContext[k2]); + return k1Sort - k2Sort; }); } -async function _getRequestAncestors(request: Request | GrpcRequest): Promise> { +async function _getRequestAncestors(request: Request | GrpcRequest | null): Promise> { return await db.withAncestors(request, [ models.request.type, models.grpcRequest.type, diff --git a/packages/insomnia-app/app/common/select-file-or-folder.js b/packages/insomnia-app/app/common/select-file-or-folder.ts similarity index 57% rename from packages/insomnia-app/app/common/select-file-or-folder.js rename to packages/insomnia-app/app/common/select-file-or-folder.ts index b7ae07aa9f..a2d88c6899 100644 --- a/packages/insomnia-app/app/common/select-file-or-folder.js +++ b/packages/insomnia-app/app/common/select-file-or-folder.ts @@ -1,52 +1,66 @@ -// @flow - -import { remote } from 'electron'; - -type Options = { - itemTypes?: Array<'file' | 'directory'>, - extensions?: Array, -}; - -type FileSelection = { - filePath: string, - canceled: boolean, -}; +import { OpenDialogOptions, remote } from 'electron'; +interface Options { + itemTypes?: Array<'file' | 'directory'>; + extensions?: Array; +} +interface FileSelection { + filePath: string; + canceled: boolean; +} const selectFileOrFolder = async ({ itemTypes, extensions }: Options): Promise => { // If no types are selected then default to just files and not directories const types = itemTypes || ['file']; let title = 'Select '; + if (types.includes('file')) { title += ' File'; + if (types.length > 2) { title += ' or'; } } + if (types.includes('directory')) { title += ' Directory'; } - const options = { + + const options: OpenDialogOptions = { title: title, buttonLabel: 'Select', + // @ts-expect-error -- TSCONVERSION we should update this to accept other properties types as well, which flow all the way up to plugins properties: types.map(type => { if (type === 'file') { return 'openFile'; } + if (type === 'directory') { return 'openDirectory'; } }), - filters: [{ name: 'All Files', extensions: ['*'] }], + filters: [ + { + name: 'All Files', + extensions: ['*'], + }, + ], }; // If extensions are provided then filter for just those extensions if (extensions?.length) { - options.filters = [{ name: 'Files', extensions: extensions }]; + options.filters = [ + { + name: 'Files', + extensions: extensions, + }, + ]; } const { canceled, filePaths } = await remote.dialog.showOpenDialog(options); - - return { filePath: filePaths[0], canceled }; + return { + filePath: filePaths[0], + canceled, + }; }; export default selectFileOrFolder; diff --git a/packages/insomnia-app/app/common/send-request.js b/packages/insomnia-app/app/common/send-request.ts similarity index 77% rename from packages/insomnia-app/app/common/send-request.js rename to packages/insomnia-app/app/common/send-request.ts index 96da3cef91..0812fde806 100644 --- a/packages/insomnia-app/app/common/send-request.js +++ b/packages/insomnia-app/app/common/send-request.ts @@ -1,28 +1,36 @@ -import * as db from './database'; -import { types as modelTypes, stats } from '../models'; +import { database as db } from './database'; +import { types as modelTypes, stats, BaseModel } from '../models'; import { send } from '../network/network'; import { getBodyBuffer } from '../models/response'; import * as plugins from '../plugins'; export async function getSendRequestCallbackMemDb(environmentId, memDB) { // Initialize the DB in-memory and fill it with data if we're given one - await db.init(modelTypes(), { inMemoryOnly: true }, true, () => {}); + await db.init( + modelTypes(), + { + inMemoryOnly: true, + }, + true, + () => {}, + ); + const docs: Array = []; - const docs = []; for (const type of Object.keys(memDB)) { for (const doc of memDB[type]) { docs.push(doc); } } - await db.batchModifyDocs({ upsert: docs, remove: [] }); - + await db.batchModifyDocs({ + upsert: docs, + remove: [], + }); // Return callback helper to send requests return async function sendRequest(requestId) { return sendAndTransform(requestId, environmentId); }; } - export function getSendRequestCallback(environmentId) { return async function sendRequest(requestId) { stats.incrementExecutedRequests(); @@ -35,13 +43,13 @@ async function sendAndTransform(requestId, environmentId) { plugins.ignorePlugin('insomnia-plugin-kong-bundle'); const res = await send(requestId, environmentId); const headersObj = {}; + for (const h of res.headers || []) { const name = h.name || ''; headersObj[name.toLowerCase()] = h.value || ''; } - const bodyBuffer = await getBodyBuffer(res); - + const bodyBuffer = await getBodyBuffer(res) as Buffer; return { status: res.statusCode, statusMessage: res.statusMessage, diff --git a/packages/insomnia-app/app/common/sorting.js b/packages/insomnia-app/app/common/sorting.ts similarity index 92% rename from packages/insomnia-app/app/common/sorting.js rename to packages/insomnia-app/app/common/sorting.ts index a0bf5aee47..903405bda1 100644 --- a/packages/insomnia-app/app/common/sorting.js +++ b/packages/insomnia-app/app/common/sorting.ts @@ -1,4 +1,3 @@ -// @flow import { HTTP_METHODS, SortOrder, @@ -57,13 +56,17 @@ const httpMethodSort: SortFunction = (a, b) => { // Sort Requests by HTTP method if (isRequest(a)) { const aIndex = HTTP_METHODS.indexOf(a.method); + // @ts-expect-error -- TSCONVERSION const bIndex = HTTP_METHODS.indexOf(b.method); + if (aIndex !== bIndex) { return aIndex < bIndex ? -1 : 1; } // Sort by ascending method name if comparing two custom methods + // @ts-expect-error -- TSCONVERSION if (aIndex === -1 && a.method.localeCompare(b.method) !== 0) { + // @ts-expect-error -- TSCONVERSION return a.method.localeCompare(b.method); } } @@ -105,7 +108,8 @@ export const descendingNumberSort = (a: number, b: number): number => { return ascendingNumberSort(b, a); }; -export const sortMethodMap: { [SortOrder]: SortFunction } = { +// @ts-expect-error -- TSCONVERSION appears to be a genuine error +export const sortMethodMap: Record = { [SORT_NAME_ASC]: ascendingNameSort, [SORT_NAME_DESC]: descendingNameSort, [SORT_CREATED_ASC]: createdFirstSort, diff --git a/packages/insomnia-app/app/common/strings.js b/packages/insomnia-app/app/common/strings.ts similarity index 96% rename from packages/insomnia-app/app/common/strings.js rename to packages/insomnia-app/app/common/strings.ts index 31b028268e..2fcae6a7d3 100644 --- a/packages/insomnia-app/app/common/strings.js +++ b/packages/insomnia-app/app/common/strings.ts @@ -1,4 +1,3 @@ -// @flow import { pluralize } from './misc'; export const strings = { diff --git a/packages/insomnia-app/app/datasets/access-token-urls.js b/packages/insomnia-app/app/datasets/access-token-urls.ts similarity index 100% rename from packages/insomnia-app/app/datasets/access-token-urls.js rename to packages/insomnia-app/app/datasets/access-token-urls.ts diff --git a/packages/insomnia-app/app/datasets/authorization-urls.js b/packages/insomnia-app/app/datasets/authorization-urls.ts similarity index 100% rename from packages/insomnia-app/app/datasets/authorization-urls.js rename to packages/insomnia-app/app/datasets/authorization-urls.ts diff --git a/packages/insomnia-app/app/datasets/charsets.js b/packages/insomnia-app/app/datasets/charsets.ts similarity index 100% rename from packages/insomnia-app/app/datasets/charsets.js rename to packages/insomnia-app/app/datasets/charsets.ts diff --git a/packages/insomnia-app/app/datasets/content-types.js b/packages/insomnia-app/app/datasets/content-types.ts similarity index 100% rename from packages/insomnia-app/app/datasets/content-types.js rename to packages/insomnia-app/app/datasets/content-types.ts diff --git a/packages/insomnia-app/app/datasets/encodings.js b/packages/insomnia-app/app/datasets/encodings.ts similarity index 100% rename from packages/insomnia-app/app/datasets/encodings.js rename to packages/insomnia-app/app/datasets/encodings.ts diff --git a/packages/insomnia-app/app/datasets/header-names.js b/packages/insomnia-app/app/datasets/header-names.ts similarity index 100% rename from packages/insomnia-app/app/datasets/header-names.js rename to packages/insomnia-app/app/datasets/header-names.ts diff --git a/packages/insomnia-app/app/global.d.ts b/packages/insomnia-app/app/global.d.ts new file mode 100644 index 0000000000..bb8e584d9f --- /dev/null +++ b/packages/insomnia-app/app/global.d.ts @@ -0,0 +1,26 @@ +declare module '*.svg' { + const content: any; + export default content; +} + +declare module '*.png' { + const content: any; + export default content; +} + +declare const __DEV__: boolean; + +declare namespace NodeJS { + interface Global { + __DEV__: boolean; + } +} + +interface Window { + __REDUX_DEVTOOLS_EXTENSION_COMPOSE__: Function; +} + +// needed for @hot-loader/react-dom in order for TypeScript to build +declare const __REACT_DEVTOOLS_GLOBAL_HOOK__: undefined | { + checkDCE: Function; +}; diff --git a/packages/insomnia-app/app/main.development.js b/packages/insomnia-app/app/main.development.ts similarity index 92% rename from packages/insomnia-app/app/main.development.js rename to packages/insomnia-app/app/main.development.ts index e69419516f..ac0f739b21 100644 --- a/packages/insomnia-app/app/main.development.js +++ b/packages/insomnia-app/app/main.development.ts @@ -1,6 +1,5 @@ -// @flow import { checkIfRestartNeeded } from './main/squirrel-startup'; -import { appConfig } from '../config'; +import appConfig from '../config/config.json'; import path from 'path'; import * as electron from 'electron'; import * as errorHandling from './main/error-handling'; @@ -8,7 +7,7 @@ import * as updates from './main/updates'; import * as grpcIpcMain from './main/grpc-ipc-main'; import * as windowUtils from './main/window-utils'; import * as models from './models/index'; -import * as database from './common/database'; +import { database } from './common/database'; import { changelogUrl, getAppVersion, isDevelopment, isMac } from './common/constants'; import type { ToastNotification } from './ui/components/toast'; import type { Stats } from './models/stats'; @@ -21,10 +20,8 @@ if (checkIfRestartNeeded()) { } initializeLogging(); - const { app, ipcMain, session } = electron; const commandLineArgs = process.argv.slice(1); - log.info(`Running version ${getAppVersion()}`); // Explicitly set userData folder from config because it's sketchy to @@ -32,32 +29,27 @@ log.info(`Running version ${getAppVersion()}`); // by accident. if (!isDevelopment()) { const defaultPath = app.getPath('userData'); - const newPath = path.join(defaultPath, '../', appConfig().userDataFolder); + const newPath = path.join(defaultPath, '../', appConfig.userDataFolder); app.setPath('userData', newPath); } // So if (window) checks don't throw global.window = global.window || undefined; - // When the app is first launched app.on('ready', async () => { // Init some important things first await database.init(models.types()); await _createModelInstances(); - await errorHandling.init(); await windowUtils.init(); - // Init the app const updatedStats = await _trackStats(); await _updateFlags(updatedStats); await _launchApp(); - // Init the rest await updates.init(); grpcIpcMain.init(); }); - // Set as default protocol app.setAsDefaultProtocolClient(`insomnia${isDevelopment() ? 'dev' : ''}`); @@ -67,19 +59,16 @@ function _addUrlToOpen(e, url) { } app.on('open-url', _addUrlToOpen); - // Enable this for CSS grid layout :) app.commandLine.appendSwitch('enable-experimental-web-platform-features'); - // Quit when all windows are closed (except on Mac). app.on('window-all-closed', () => { if (!isMac()) { app.quit(); } }); - // Mac-only, when the user clicks the doc icon -app.on('activate', (e, hasVisibleWindows) => { +app.on('activate', (_error, hasVisibleWindows) => { // Create a new window when clicking the doc icon if there isn't one open if (!hasVisibleWindows) { try { @@ -95,12 +84,11 @@ app.on('activate', (e, hasVisibleWindows) => { function _launchApp() { app.removeListener('open-url', _addUrlToOpen); const window = windowUtils.createWindow(); - // Handle URLs sent via command line args ipcMain.once('window-ready', () => { + // @ts-expect-error -- TSCONVERSION commandLineArgs.length && window.send('run-command', commandLineArgs[0]); }); - // Called when second instance launched with args (Windows) const gotTheLock = app.requestSingleInstanceLock(); @@ -109,16 +97,16 @@ function _launchApp() { return; } - app.on('second-instance', (event, commandLine, workingDirectory) => { + app.on('second-instance', () => { // Someone tried to run a second instance, we should focus our window. if (window) { if (window.isMinimized()) window.restore(); window.focus(); } }); - // Handle URLs when app already open - app.addListener('open-url', (e, url) => { + app.addListener('open-url', (_error, url) => { + // @ts-expect-error -- TSCONVERSION window.send('run-command', url); // Apparently a timeout is needed because Chrome steals back focus immediately // after opening the URL. @@ -126,7 +114,6 @@ function _launchApp() { window.focus(); }, 100); }); - // Don't send origin header from Insomnia app because we're not technically using CORS session.defaultSession.webRequest.onBeforeSendHeaders((details, fn) => { delete details.requestHeaders.Origin; @@ -149,6 +136,7 @@ async function _createModelInstances() { async function _updateFlags({ launches }: Stats) { const firstLaunch = launches === 1; + if (firstLaunch) { await models.settings.patch({ hasPromptedOnboarding: false, @@ -158,7 +146,7 @@ async function _updateFlags({ launches }: Stats) { } } -async function _trackStats(): Promise { +async function _trackStats() { // Handle the stats const oldStats = await models.stats.get(); const stats: Stats = await models.stats.update({ @@ -168,7 +156,6 @@ async function _trackStats(): Promise { lastVersion: oldStats.currentVersion, launches: oldStats.launches + 1, }); - // Update Stats Object const firstLaunch = stats.launches === 1; const justUpdated = !firstLaunch && stats.currentVersion !== stats.lastVersion; @@ -183,6 +170,7 @@ async function _trackStats(): Promise { ipcMain.once('window-ready', () => { const { currentVersion } = stats; + if (!justUpdated || !currentVersion) { return; } @@ -194,14 +182,13 @@ async function _trackStats(): Promise { cta: "See What's New", message: `Updated to ${currentVersion}`, }; - // Wait a bit before showing the user because the app just launched. setTimeout(() => { for (const window of BrowserWindow.getAllWindows()) { + // @ts-expect-error -- TSCONVERSION window.send('show-notification', notification); } }, 5000); }); - return stats; } diff --git a/packages/insomnia-app/app/main/__tests__/grpc-ipc-main.test.js b/packages/insomnia-app/app/main/__tests__/grpc-ipc-main.test.ts similarity index 97% rename from packages/insomnia-app/app/main/__tests__/grpc-ipc-main.test.js rename to packages/insomnia-app/app/main/__tests__/grpc-ipc-main.test.ts index 55fe91c4bc..a51abffc8f 100644 --- a/packages/insomnia-app/app/main/__tests__/grpc-ipc-main.test.js +++ b/packages/insomnia-app/app/main/__tests__/grpc-ipc-main.test.ts @@ -7,9 +7,10 @@ import { ResponseCallbacks } from '../../network/grpc/response-callbacks'; jest.mock('../../network/grpc'); describe('grpcIpcMain', () => { - const event = { reply: jest.fn() }; + const event = { + reply: jest.fn(), + }; const id = 'abc'; - beforeEach(() => { grpcIpcMain.init(); // ipcMain is mocked }); @@ -20,11 +21,8 @@ describe('grpcIpcMain', () => { it('should add expected listener for start', () => { const [channel, listener] = ipcMain.on.mock.calls[0]; - expect(channel).toBe(GrpcRequestEventEnum.start); - const param = {}; - // Execute the callback, and make sure the correct grpc method is called listener(event, param); expect(grpc.start).toHaveBeenCalledWith(param, expect.any(ResponseCallbacks)); @@ -32,12 +30,9 @@ describe('grpcIpcMain', () => { it('should add expected listener for sendMessage', () => { const [channel, listener] = ipcMain.on.mock.calls[1]; - // Expect the sendUnary channel expect(channel).toBe(GrpcRequestEventEnum.sendMessage); - const param = {}; - // Execute the callback, and make sure the correct grpc method is called listener(event, param); expect(grpc.sendMessage).toHaveBeenCalledWith(param, expect.any(ResponseCallbacks)); @@ -45,9 +40,7 @@ describe('grpcIpcMain', () => { it('should add expected listener for commit', () => { const [channel, listener] = ipcMain.on.mock.calls[2]; - expect(channel).toBe(GrpcRequestEventEnum.commit); - // Execute the callback, and make sure the correct grpc method is called listener(event, id); expect(grpc.commit).toHaveBeenCalledWith(id); @@ -55,9 +48,7 @@ describe('grpcIpcMain', () => { it('should add expected listener for cancel', () => { const [channel, listener] = ipcMain.on.mock.calls[3]; - expect(channel).toBe(GrpcRequestEventEnum.cancel); - // Execute the callback, and make sure the correct grpc method is called listener(event, id); expect(grpc.cancel).toHaveBeenCalledWith(id); @@ -65,9 +56,7 @@ describe('grpcIpcMain', () => { it('should add expected listener for cancel multiple', () => { const [channel, listener] = ipcMain.on.mock.calls[4]; - expect(channel).toBe(GrpcRequestEventEnum.cancelMultiple); - // Execute the callback, and make sure the correct grpc method is called listener(event, id); expect(grpc.cancelMultiple).toHaveBeenCalledWith(id); diff --git a/packages/insomnia-app/app/main/error-handling.js b/packages/insomnia-app/app/main/error-handling.ts similarity index 79% rename from packages/insomnia-app/app/main/error-handling.js rename to packages/insomnia-app/app/main/error-handling.ts index e1a3f31ebf..36ce02396a 100644 --- a/packages/insomnia-app/app/main/error-handling.js +++ b/packages/insomnia-app/app/main/error-handling.ts @@ -2,8 +2,7 @@ export function init() { process.on('uncaughtException', err => { console.error('[catcher] Uncaught exception:', err.stack); }); - - process.on('unhandledRejection', err => { + process.on('unhandledRejection', (err: Error) => { console.error('[catcher] Unhandled rejection:', err.stack); }); } diff --git a/packages/insomnia-app/app/main/grpc-ipc-main.js b/packages/insomnia-app/app/main/grpc-ipc-main.ts similarity index 88% rename from packages/insomnia-app/app/main/grpc-ipc-main.js rename to packages/insomnia-app/app/main/grpc-ipc-main.ts index 50165c8b96..c28e560f56 100644 --- a/packages/insomnia-app/app/main/grpc-ipc-main.js +++ b/packages/insomnia-app/app/main/grpc-ipc-main.ts @@ -1,16 +1,15 @@ -// @flow - import * as grpc from '../network/grpc'; import { ipcMain } from 'electron'; import { GrpcRequestEventEnum } from '../common/grpc-events'; import { ResponseCallbacks } from '../network/grpc/response-callbacks'; -import type { GrpcIpcRequestParams } from '../ui/context/grpc/prepare'; +import { GrpcIpcRequestParams } from '../network/grpc/prepare'; export function init() { ipcMain.on(GrpcRequestEventEnum.start, (e, params: GrpcIpcRequestParams) => grpc.start(params, new ResponseCallbacks(e)), ); ipcMain.on(GrpcRequestEventEnum.sendMessage, (e, params: GrpcIpcRequestParams) => + // @ts-expect-error -- TSCONVERSION grpc.sendMessage(params, new ResponseCallbacks(e)), ); ipcMain.on(GrpcRequestEventEnum.commit, (_, requestId) => grpc.commit(requestId)); diff --git a/packages/insomnia-app/app/main/local-storage.js b/packages/insomnia-app/app/main/local-storage.ts similarity index 83% rename from packages/insomnia-app/app/main/local-storage.js rename to packages/insomnia-app/app/main/local-storage.ts index adb19aa3c1..cf27be3c92 100644 --- a/packages/insomnia-app/app/main/local-storage.js +++ b/packages/insomnia-app/app/main/local-storage.ts @@ -3,13 +3,13 @@ import fs from 'fs'; import path from 'path'; class LocalStorage { - constructor(basePath) { + _buffer: Record = {}; + _timeouts: Record = {}; + _basePath: string | null = null; + + constructor(basePath: string) { this._basePath = basePath; - // Debounce writes on a per key basis - this._timeouts = {}; - this._buffer = {}; - mkdirp.sync(basePath); console.log(`[localstorage] Initialized at ${basePath}`); } @@ -27,8 +27,9 @@ class LocalStorage { let contents = JSON.stringify(defaultObj); const path = this._getKeyPath(key); + try { - contents = fs.readFileSync(path); + contents = String(fs.readFileSync(path)); } catch (e) { if (e.code === 'ENOENT') { this.setItem(key, defaultObj); @@ -52,6 +53,7 @@ class LocalStorage { for (const key of keys) { const contents = this._buffer[key]; + const path = this._getKeyPath(key); delete this._buffer[key]; @@ -65,6 +67,7 @@ class LocalStorage { } _getKeyPath(key) { + // @ts-expect-error -- TSCONVERSION this appears to be a genuine error return path.join(this._basePath, key); } } diff --git a/packages/insomnia-app/app/main/squirrel-startup.js b/packages/insomnia-app/app/main/squirrel-startup.ts similarity index 91% rename from packages/insomnia-app/app/main/squirrel-startup.js rename to packages/insomnia-app/app/main/squirrel-startup.ts index e1c26b0bef..6a3afb1a26 100644 --- a/packages/insomnia-app/app/main/squirrel-startup.js +++ b/packages/insomnia-app/app/main/squirrel-startup.ts @@ -4,7 +4,9 @@ import { app } from 'electron'; function run(args, done) { const updateExe = path.resolve(path.dirname(process.execPath), '..', 'Update.exe'); - spawn(updateExe, args, { detached: true }).on('close', done); + spawn(updateExe, args, { + detached: true, + }).on('close', done); } export function checkIfRestartNeeded() { @@ -20,9 +22,11 @@ export function checkIfRestartNeeded() { case '--squirrel-install': run(['--createShortcut=' + target + ''], app.quit); return true; + case '--squirrel-uninstall': run(['--removeShortcut=' + target + ''], app.quit); return true; + case '--squirrel-updated': case '--squirrel-obsolete': app.quit(); diff --git a/packages/insomnia-app/app/main/updates.js b/packages/insomnia-app/app/main/updates.ts similarity index 82% rename from packages/insomnia-app/app/main/updates.js rename to packages/insomnia-app/app/main/updates.ts index a7b32e80c7..d6e2c6f1db 100644 --- a/packages/insomnia-app/app/main/updates.js +++ b/packages/insomnia-app/app/main/updates.ts @@ -1,4 +1,3 @@ -// @flow import electron from 'electron'; import { CHECK_FOR_UPDATES_INTERVAL, @@ -12,13 +11,12 @@ import { import * as models from '../models/index'; import { buildQueryStringFromParams, joinUrlAndQueryString } from 'insomnia-url'; import { delay } from '../common/misc'; - const { autoUpdater, BrowserWindow, ipcMain } = electron; async function getUpdateUrl(force: boolean): Promise { const platform = process.platform; const settings = await models.settings.getOrCreate(); - let updateUrl = null; + let updateUrl: string | null = null; if (!updatesSupported()) { return null; @@ -33,11 +31,19 @@ async function getUpdateUrl(force: boolean): Promise { } const params = [ - { name: 'v', value: getAppVersion() }, - { name: 'app', value: getAppId() }, - { name: 'channel', value: settings.updateChannel }, + { + name: 'v', + value: getAppVersion(), + }, + { + name: 'app', + value: getAppId(), + }, + { + name: 'channel', + value: settings.updateChannel, + }, ]; - const qs = buildQueryStringFromParams(params); const fullUrl = joinUrlAndQueryString(updateUrl, qs); console.log(`[updater] Using url ${fullUrl}`); @@ -60,50 +66,51 @@ async function getUpdateUrl(force: boolean): Promise { function _sendUpdateStatus(status) { const windows = BrowserWindow.getAllWindows(); - for (const w of windows) { - w.send('updater.check.status', status); + + for (const window of windows) { + // @ts-expect-error -- TSCONVERSION seems to be a genuine error + window.send('updater.check.status', status); } } function _sendUpdateComplete(success: boolean, msg: string) { const windows = BrowserWindow.getAllWindows(); - for (const w of windows) { - w.send('updater.check.complete', success, msg); + + for (const window of windows) { + // @ts-expect-error -- TSCONVERSION seems to be a genuine error + window.send('updater.check.complete', success, msg); } } let hasPromptedForUpdates = false; - export async function init() { autoUpdater.on('error', e => { console.warn(`[updater] Error: ${e.message}`); }); - autoUpdater.on('update-not-available', () => { console.log('[updater] Not Available'); + _sendUpdateComplete(false, 'Up to Date'); }); - autoUpdater.on('update-available', () => { console.log('[updater] Update Available'); + _sendUpdateStatus('Downloading...'); }); - - autoUpdater.on('update-downloaded', (e, releaseNotes, releaseName, releaseDate, updateUrl) => { + autoUpdater.on('update-downloaded', (_error, _releaseNotes, releaseName) => { console.log(`[updater] Downloaded ${releaseName}`); + _sendUpdateComplete(true, 'Updated (Restart Required)'); + _showUpdateNotification(); }); - - ipcMain.on('updater.check', async e => { + ipcMain.on('updater.check', async () => { await _checkForUpdates(true); }); - // Check for updates on an interval setInterval(async () => { await _checkForUpdates(false); }, CHECK_FOR_UPDATES_INTERVAL); - // Check for updates immediately await _checkForUpdates(false); } @@ -114,6 +121,7 @@ function _showUpdateNotification() { } const windows = BrowserWindow.getAllWindows(); + if (windows.length && windows[0].webContents) { windows[0].webContents.send('update-available'); } @@ -123,6 +131,7 @@ function _showUpdateNotification() { async function _checkForUpdates(force: boolean) { _sendUpdateStatus('Checking'); + await delay(500); if (force) { @@ -140,16 +149,20 @@ async function _checkForUpdates(force: boolean) { console.log( `[updater] Updater not running platform=${process.platform} dev=${isDevelopment()}`, ); + _sendUpdateComplete(false, 'Updates Not Supported'); + return; } try { console.log(`[updater] Checking for updates url=${updateUrl}`); + // @ts-expect-error -- TSCONVERSION appears to be a genuine error autoUpdater.setFeedURL(updateUrl); autoUpdater.checkForUpdates(); } catch (err) { console.warn('[updater] Failed to check for updates:', err.message); + _sendUpdateComplete(false, 'Update Error'); } } diff --git a/packages/insomnia-app/app/main/window-utils.js b/packages/insomnia-app/app/main/window-utils.ts similarity index 65% rename from packages/insomnia-app/app/main/window-utils.js rename to packages/insomnia-app/app/main/window-utils.ts index 83a78d4588..010e51b292 100644 --- a/packages/insomnia-app/app/main/window-utils.js +++ b/packages/insomnia-app/app/main/window-utils.ts @@ -1,4 +1,4 @@ -import electron from 'electron'; +import electron, { BrowserWindow } from 'electron'; import path from 'path'; import { Curl } from 'node-libcurl'; import fs from 'fs'; @@ -14,25 +14,34 @@ import { isMac, MNEMONIC_SYM, } from '../common/constants'; -import * as misc from '../common/misc'; +import { clickLink, getDataDirectory } from '../common/electron-helpers'; import * as log from '../common/log'; import * as os from 'os'; import { docsBase } from '../common/documentation'; -const { app, Menu, BrowserWindow, shell, dialog, clipboard } = electron; - +const { app, Menu, shell, dialog, clipboard } = electron; // So we can use native modules in renderer // NOTE: This will be deprecated in Electron 10 and impossible in 11 // https://github.com/electron/electron/issues/18397 app.allowRendererProcessReuse = false; +// Note: this hack is required because MenuItemConstructorOptions is not exported from the electron types as of 9.3.5 +type MenuItemConstructorOptions = Parameters[0][0]; + const DEFAULT_WIDTH = 1280; const DEFAULT_HEIGHT = 700; const MINIMUM_WIDTH = 500; const MINIMUM_HEIGHT = 400; -let mainWindow = null; -let localStorage = null; +let mainWindow: BrowserWindow | null = null; +let localStorage: LocalStorage | null = null; + +interface Bounds { + height?: number; + width?: number; + x?: number; + y?: number; +} export function init() { initLocalStorage(); @@ -43,14 +52,19 @@ export function createWindow() { const zoomFactor = getZoomFactor(); const { bounds, fullscreen, maximize } = getBounds(); const { x, y, width, height } = bounds; - const appLogo = 'static/insomnia-core-logo_16x.png'; + const appLogo = 'static/insomnia-core-logo_16x.png'; let isVisibleOnAnyDisplay = true; + for (const d of electron.screen.getAllDisplays()) { const isVisibleOnDisplay = + // @ts-expect-error -- TSCONVERSION genuine error x >= d.bounds.x && + // @ts-expect-error -- TSCONVERSION genuine error y >= d.bounds.y && + // @ts-expect-error -- TSCONVERSION genuine error x + width <= d.bounds.x + d.bounds.width && + // @ts-expect-error -- TSCONVERSION genuine error y + height <= d.bounds.y + d.bounds.height; if (!isVisibleOnDisplay) { @@ -62,7 +76,6 @@ export function createWindow() { // Make sure we don't initialize the window outside the bounds x: isVisibleOnAnyDisplay ? x : undefined, y: isVisibleOnAnyDisplay ? y : undefined, - // Other options backgroundColor: '#2C2C2C', fullscreen: fullscreen, @@ -84,23 +97,18 @@ export function createWindow() { // BrowserWindow doesn't have an option for this, so we have to do it manually :( if (maximize) { - mainWindow.maximize(); + mainWindow?.maximize(); } - mainWindow.on('resize', e => saveBounds()); - - mainWindow.on('maximize', e => saveBounds()); - - mainWindow.on('unmaximize', e => saveBounds()); - - mainWindow.on('move', e => saveBounds()); - - mainWindow.on('unresponsive', e => { + mainWindow?.on('resize', () => saveBounds()); + mainWindow?.on('maximize', () => saveBounds()); + mainWindow?.on('unmaximize', () => saveBounds()); + mainWindow?.on('move', () => saveBounds()); + mainWindow?.on('unresponsive', () => { showUnresponsiveModal(); }); - // Open generic links () in default browser - mainWindow.webContents.on('will-navigate', (e, url) => { + mainWindow?.webContents.on('will-navigate', (e, url) => { if (url === appUrl) { return; } @@ -109,76 +117,121 @@ export function createWindow() { e.preventDefault(); electron.shell.openExternal(url); }); - // Load the html of the app. const url = process.env.APP_RENDER_URL; const appUrl = url || `file://${app.getAppPath()}/renderer.html`; console.log(`[main] Loading ${appUrl}`); - mainWindow.loadURL(appUrl); - + mainWindow?.loadURL(appUrl); // Emitted when the window is closed. - mainWindow.on('closed', () => { + mainWindow?.on('closed', () => { // Dereference the window object, usually you would store windows // in an array if your app supports multi windows, this is the time // when you should delete the corresponding element. mainWindow = null; }); - const applicationMenu = { + const applicationMenu: MenuItemConstructorOptions = { label: `${MNEMONIC_SYM}Application`, submenu: [ { label: `${MNEMONIC_SYM}Preferences`, - click: function(menuItem, window, e) { + click: function(_menuItem, window) { if (!window || !window.webContents) { return; } + window.webContents.send('toggle-preferences'); }, }, { label: `${MNEMONIC_SYM}Changelog`, - click: function(menuItem, window, e) { + click: function(_menuItem, window) { if (!window || !window.webContents) { return; } - misc.clickLink(changelogUrl()); + + clickLink(changelogUrl()); }, }, - { type: 'separator' }, - { role: 'hide' }, - { role: 'hideothers' }, - { type: 'separator' }, - { label: `${MNEMONIC_SYM}Quit`, accelerator: 'CmdOrCtrl+Q', click: () => app.quit() }, + { + type: 'separator', + }, + { + role: 'hide', + }, + { + // @ts-expect-error -- TSCONVERSION appears to be a genuine error + role: 'hideothers', + }, + { + type: 'separator', + }, + { + label: `${MNEMONIC_SYM}Quit`, + accelerator: 'CmdOrCtrl+Q', + click: () => app.quit(), + }, ], }; - const editMenu = { + const editMenu: MenuItemConstructorOptions = { label: `${MNEMONIC_SYM}Edit`, submenu: [ - { label: `${MNEMONIC_SYM}Undo`, accelerator: 'CmdOrCtrl+Z', selector: 'undo:' }, - { label: `${MNEMONIC_SYM}Redo`, accelerator: 'Shift+CmdOrCtrl+Z', selector: 'redo:' }, - { type: 'separator' }, - { label: `Cu${MNEMONIC_SYM}t`, accelerator: 'CmdOrCtrl+X', selector: 'cut:' }, - { label: `${MNEMONIC_SYM}Copy`, accelerator: 'CmdOrCtrl+C', selector: 'copy:' }, - { label: `${MNEMONIC_SYM}Paste`, accelerator: 'CmdOrCtrl+V', selector: 'paste:' }, + { + label: `${MNEMONIC_SYM}Undo`, + accelerator: 'CmdOrCtrl+Z', + // @ts-expect-error -- TSCONVERSION missing in official electron types + selector: 'undo:', + }, + { + label: `${MNEMONIC_SYM}Redo`, + accelerator: 'Shift+CmdOrCtrl+Z', + // @ts-expect-error -- TSCONVERSION missing in official electron types + selector: 'redo:', + }, + { + type: 'separator', + }, + { + label: `Cu${MNEMONIC_SYM}t`, + accelerator: 'CmdOrCtrl+X', + // @ts-expect-error -- TSCONVERSION missing in official electron types + selector: 'cut:', + }, + { + label: `${MNEMONIC_SYM}Copy`, + accelerator: 'CmdOrCtrl+C', + // @ts-expect-error -- TSCONVERSION missing in official electron types + selector: 'copy:', + }, + { + label: `${MNEMONIC_SYM}Paste`, + accelerator: 'CmdOrCtrl+V', + // @ts-expect-error -- TSCONVERSION missing in official electron types + selector: 'paste:', + }, { label: `Select ${MNEMONIC_SYM}All`, accelerator: 'CmdOrCtrl+A', + // @ts-expect-error -- TSCONVERSION missing in official electron types selector: 'selectAll:', }, ], }; - const viewMenu = { + const viewMenu: MenuItemConstructorOptions = { label: `${MNEMONIC_SYM}View`, submenu: [ - { label: `Toggle ${MNEMONIC_SYM}Full Screen`, role: 'togglefullscreen' }, + { + label: `Toggle ${MNEMONIC_SYM}Full Screen`, + role: 'togglefullscreen', + }, { label: `${MNEMONIC_SYM}Actual Size`, accelerator: 'CmdOrCtrl+0', click: () => { const w = BrowserWindow.getFocusedWindow(); + if (!w || !w.webContents) { return; } @@ -193,13 +246,13 @@ export function createWindow() { accelerator: 'CmdOrCtrl+=', click: () => { const w = BrowserWindow.getFocusedWindow(); + if (!w || !w.webContents) { return; } const zoomFactor = Math.min(1.8, getZoomFactor() + 0.05); w.webContents.setZoomFactor(zoomFactor); - saveZoomFactor(zoomFactor); }, }, @@ -208,6 +261,7 @@ export function createWindow() { accelerator: 'CmdOrCtrl+-', click: () => { const w = BrowserWindow.getFocusedWindow(); + if (!w || !w.webContents) { return; } @@ -221,6 +275,7 @@ export function createWindow() { label: 'Toggle Sidebar', click: () => { const w = BrowserWindow.getFocusedWindow(); + if (!w || !w.webContents) { return; } @@ -231,27 +286,39 @@ export function createWindow() { { label: `Toggle ${MNEMONIC_SYM}DevTools`, accelerator: 'Alt+CmdOrCtrl+I', - click: () => mainWindow.toggleDevTools(), + // @ts-expect-error -- TSCONVERSION needs global module augmentation + click: () => mainWindow?.toggleDevTools(), }, ], }; - const windowMenu = { + const windowMenu: MenuItemConstructorOptions = { label: `${MNEMONIC_SYM}Window`, role: 'window', submenu: [ - { label: `${MNEMONIC_SYM}Minimize`, role: 'minimize' }, - ...(isMac() ? [{ label: `${MNEMONIC_SYM}Close`, role: 'close' }] : []), + { + label: `${MNEMONIC_SYM}Minimize`, + role: 'minimize', + }, + // @ts-expect-error -- TSCONVERSION missing in official electron types + ...(isMac() ? [ + { + label: `${MNEMONIC_SYM}Close`, + role: 'close', + }, + ] + : []), ], }; - const helpMenu = { + const helpMenu: MenuItemConstructorOptions = { label: `${MNEMONIC_SYM}Help`, role: 'help', id: 'help', submenu: [ { label: `${MNEMONIC_SYM}Help and Support`, + // @ts-expect-error -- TSCONVERSION TSCONVERSION `Accelerator` type from electron is needed here as a cast but is not exported as of the 9.3.5 types accelerator: !isMac() ? 'F1' : null, click: () => { shell.openExternal(docsBase); @@ -260,30 +327,31 @@ export function createWindow() { { label: `${MNEMONIC_SYM}Keyboard Shortcuts`, accelerator: 'CmdOrCtrl+Shift+?', - click: (menuItem, w, e) => { + click: (_menuItem, w) => { if (!w || !w.webContents) { return; } + w.webContents.send('toggle-preferences-shortcuts'); }, }, { label: `Show App ${MNEMONIC_SYM}Data Folder`, - click: (menuItem, w, e) => { - const directory = misc.getDataDirectory(); + click: () => { + const directory = getDataDirectory(); shell.showItemInFolder(directory); }, }, { label: `Show App ${MNEMONIC_SYM}Logs Folder`, - click: (menuItem, w, e) => { + click: () => { const directory = log.getLogDirectory(); shell.showItemInFolder(directory); }, }, { label: 'Show Open Source Licenses', - click: (menuItem, w, e) => { + click: () => { const licensePath = path.resolve(app.getAppPath(), '../opensource-licenses.txt'); shell.openPath(licensePath); }, @@ -300,9 +368,7 @@ export function createWindow() { const aboutMenuClickHandler = async () => { const copy = 'Copy'; const ok = 'OK'; - const buttons = isLinux() ? [copy, ok] : [ok, copy]; - const detail = [ `Version: ${getAppLongName()} ${getAppVersion()}`, `Release date: ${getAppReleaseDate()}`, @@ -331,33 +397,38 @@ export function createWindow() { }; if (isMac()) { - applicationMenu.submenu.unshift( + // @ts-expect-error -- TSCONVERSION type splitting + applicationMenu.submenu?.unshift( { label: `A${MNEMONIC_SYM}bout ${getAppName()}`, click: aboutMenuClickHandler, }, - { type: 'separator' }, + { + type: 'separator', + }, ); } else { - helpMenu.submenu.push({ + // @ts-expect-error -- TSCONVERSION type splitting + helpMenu.submenu?.push({ label: `${MNEMONIC_SYM}About`, click: aboutMenuClickHandler, }); } - const developerMenu = { + const developerMenu: MenuItemConstructorOptions = { label: `${MNEMONIC_SYM}Developer`, + // @ts-expect-error -- TSCONVERSION missing in official electron types position: 'before=help', submenu: [ { label: `${MNEMONIC_SYM}Reload`, accelerator: 'Shift+F5', - click: () => mainWindow.reload(), + click: () => mainWindow?.reload(), }, { label: `Resize to Defaul${MNEMONIC_SYM}t`, click: () => - mainWindow.setBounds({ + mainWindow?.setBounds({ x: 100, y: 100, width: DEFAULT_WIDTH, @@ -367,7 +438,8 @@ export function createWindow() { { label: `Take ${MNEMONIC_SYM}Screenshot`, click: function() { - mainWindow.capturePage(image => { + // @ts-expect-error -- TSCONVERSION not accounted for in the electron types to provide a function + mainWindow?.capturePage(image => { const buffer = image.toPNG(); const dir = app.getPath('desktop'); fs.writeFileSync(path.join(dir, `Screenshot-${new Date()}.png`), buffer); @@ -384,14 +456,14 @@ export function createWindow() { }, ], }; - - const toolsMenu = { + const toolsMenu: MenuItemConstructorOptions = { label: `${MNEMONIC_SYM}Tools`, submenu: [ { label: `${MNEMONIC_SYM}Reload Plugins`, click: () => { const w = BrowserWindow.getFocusedWindow(); + if (!w || !w.webContents) { return; } @@ -401,9 +473,7 @@ export function createWindow() { }, ], }; - - const template = []; - + const template: Array = []; template.push(applicationMenu); template.push(editMenu); template.push(viewMenu); @@ -416,7 +486,6 @@ export function createWindow() { } Menu.setApplicationMenu(Menu.buildFromTemplate(template)); - return mainWindow; } @@ -430,8 +499,9 @@ async function showUnresponsiveModal() { message: 'Insomnia has become unresponsive. Do you want to reload?', }); + // @ts-expect-error -- TSCONVERSION appears to be a genuine error if (id === 1) { - mainWindow.destroy(); + mainWindow?.destroy(); createWindow(); } } @@ -441,42 +511,48 @@ function saveBounds() { return; } - const fullscreen = mainWindow.isFullScreen(); + const fullscreen = mainWindow?.isFullScreen(); // Only save the size if we're not in fullscreen if (!fullscreen) { - localStorage.setItem('bounds', mainWindow.getBounds()); - localStorage.setItem('maximize', mainWindow.isMaximized()); - localStorage.setItem('fullscreen', false); + localStorage?.setItem('bounds', mainWindow?.getBounds()); + localStorage?.setItem('maximize', mainWindow?.isMaximized()); + localStorage?.setItem('fullscreen', false); } else { - localStorage.setItem('fullscreen', true); + localStorage?.setItem('fullscreen', true); } } function getBounds() { - let bounds = {}; + let bounds: Bounds = {}; let fullscreen = false; let maximize = false; + try { - bounds = localStorage.getItem('bounds', {}); - fullscreen = localStorage.getItem('fullscreen', false); - maximize = localStorage.getItem('maximize', false); + bounds = localStorage?.getItem('bounds', {}); + fullscreen = localStorage?.getItem('fullscreen', false); + maximize = localStorage?.getItem('maximize', false); } catch (e) { // This should never happen, but if it does...! console.error('Failed to parse window bounds', e); } - return { bounds, fullscreen, maximize }; + return { + bounds, + fullscreen, + maximize, + }; } function saveZoomFactor(zoomFactor) { - localStorage.setItem('zoomFactor', zoomFactor); + localStorage?.setItem('zoomFactor', zoomFactor); } function getZoomFactor() { let zoomFactor = 1; + try { - zoomFactor = localStorage.getItem('zoomFactor', 1); + zoomFactor = localStorage?.getItem('zoomFactor', 1); } catch (e) { // This should never happen, but if it does...! console.error('Failed to parse zoomFactor', e); @@ -486,10 +562,11 @@ function getZoomFactor() { } function initLocalStorage() { - const localStoragePath = path.join(misc.getDataDirectory(), 'localStorage'); + const localStoragePath = path.join(getDataDirectory(), 'localStorage'); localStorage = new LocalStorage(localStoragePath); } function initContextMenus() { + // eslint-disable-next-line @typescript-eslint/no-var-requires require('electron-context-menu')({}); } diff --git a/packages/insomnia-app/app/models/__mocks__/uuid.js b/packages/insomnia-app/app/models/__mocks__/uuid.ts similarity index 98% rename from packages/insomnia-app/app/models/__mocks__/uuid.js rename to packages/insomnia-app/app/models/__mocks__/uuid.ts index 0a00c44f88..beab054650 100644 --- a/packages/insomnia-app/app/models/__mocks__/uuid.js +++ b/packages/insomnia-app/app/models/__mocks__/uuid.ts @@ -1,4 +1,5 @@ let v1Counter = 0; + let v4Counter = 0; const v1UUIDs = [ @@ -210,6 +211,7 @@ function v4() { return uuid; } +// WARNING: changing this to `export default` will break the mock and be incredibly hard to debug. Ask me how I know. module.exports = () => v4(); module.exports.v4 = () => v4(); module.exports.v1 = () => v1(); diff --git a/packages/insomnia-app/app/models/__tests__/grpc-request-meta.test.js b/packages/insomnia-app/app/models/__tests__/grpc-request-meta.test.ts similarity index 83% rename from packages/insomnia-app/app/models/__tests__/grpc-request-meta.test.js rename to packages/insomnia-app/app/models/__tests__/grpc-request-meta.test.ts index b1edac2b65..a0bc3ac8e5 100644 --- a/packages/insomnia-app/app/models/__tests__/grpc-request-meta.test.js +++ b/packages/insomnia-app/app/models/__tests__/grpc-request-meta.test.ts @@ -3,6 +3,7 @@ import { globalBeforeEach } from '../../__jest__/before-each'; describe('init()', () => { beforeEach(globalBeforeEach); + it('contains all required fields', async () => { expect(models.grpcRequestMeta.init()).toEqual({ pinned: false, @@ -13,9 +14,9 @@ describe('init()', () => { describe('create()', () => { beforeEach(globalBeforeEach); + it('creates a valid GrpcRequest', async () => { Date.now = jest.fn().mockReturnValue(1478795580200); - const request = await models.grpcRequestMeta.create({ pinned: true, parentId: 'greq_124', @@ -29,14 +30,12 @@ describe('create()', () => { type: 'GrpcRequestMeta', lastActive: 0, }; - expect(request).toEqual(expected); expect(await models.grpcRequestMeta.getOrCreateByParentId(expected.parentId)).toEqual(expected); }); it('creates a valid GrpcRequestMeta if it does not exist', async () => { Date.now = jest.fn().mockReturnValue(1478795580200); - const request = await models.grpcRequestMeta.getOrCreateByParentId('greq_124'); const expected = { _id: 'greqm_dd2ccc1a2745477a881a9e8ef9d42403', @@ -47,19 +46,22 @@ describe('create()', () => { type: 'GrpcRequestMeta', lastActive: 0, }; - expect(request).toEqual(expected); }); it('fails when missing parentId', async () => { - expect(() => models.grpcRequestMeta.create({ pinned: true })).toThrow( - 'New GrpcRequestMeta missing `parentId`', - ); + expect(() => + models.grpcRequestMeta.create({ + pinned: true, + }), + ).toThrow('New GrpcRequestMeta missing `parentId`'); }); it('fails when parentId prefix is not that of a GrpcRequest', async () => { - expect(() => models.grpcRequestMeta.create({ parentId: 'req_123' })).toThrow( - 'Expected the parent of GrpcRequestMeta to be a GrpcRequest', - ); + expect(() => + models.grpcRequestMeta.create({ + parentId: 'req_123', + }), + ).toThrow('Expected the parent of GrpcRequestMeta to be a GrpcRequest'); }); }); diff --git a/packages/insomnia-app/app/models/__tests__/grpc-request.test.js b/packages/insomnia-app/app/models/__tests__/grpc-request.test.ts similarity index 88% rename from packages/insomnia-app/app/models/__tests__/grpc-request.test.js rename to packages/insomnia-app/app/models/__tests__/grpc-request.test.ts index e890322432..928ba772a3 100644 --- a/packages/insomnia-app/app/models/__tests__/grpc-request.test.js +++ b/packages/insomnia-app/app/models/__tests__/grpc-request.test.ts @@ -3,9 +3,9 @@ import { globalBeforeEach } from '../../__jest__/before-each'; describe('init()', () => { beforeEach(globalBeforeEach); + it('contains all required fields', async () => { Date.now = jest.fn().mockReturnValue(1478795580200); - expect(models.grpcRequest.init()).toEqual({ url: '', name: 'New gRPC Request', @@ -16,16 +16,16 @@ describe('init()', () => { text: '{}', }, metaSortKey: -1478795580200, - idPrivate: false, + isPrivate: false, }); }); }); describe('create()', () => { beforeEach(globalBeforeEach); + it('creates a valid GrpcRequest', async () => { Date.now = jest.fn().mockReturnValue(1478795580200); - const request = await models.grpcRequest.create({ name: 'My request', parentId: 'fld_124', @@ -44,18 +44,19 @@ describe('create()', () => { text: '{}', }, metaSortKey: -1478795580200, - idPrivate: false, + isPrivate: false, type: 'GrpcRequest', }; - expect(request).toEqual(expected); expect(await models.grpcRequest.getById(expected._id)).toEqual(expected); }); it('fails when missing parentId', async () => { Date.now = jest.fn().mockReturnValue(1478795580200); - expect(() => models.grpcRequest.create({ name: 'no parentId' })).toThrow( - 'New GrpcRequest missing `parentId`', - ); + expect(() => + models.grpcRequest.create({ + name: 'no parentId', + }), + ).toThrow('New GrpcRequest missing `parentId`'); }); }); diff --git a/packages/insomnia-app/app/models/__tests__/index.test.js b/packages/insomnia-app/app/models/__tests__/index.test.ts similarity index 98% rename from packages/insomnia-app/app/models/__tests__/index.test.js rename to packages/insomnia-app/app/models/__tests__/index.test.ts index 44a6361890..0c22d51eb9 100644 --- a/packages/insomnia-app/app/models/__tests__/index.test.js +++ b/packages/insomnia-app/app/models/__tests__/index.test.ts @@ -1,12 +1,9 @@ -// @flow - import { globalBeforeEach } from '../../__jest__/before-each'; import { getModel, mustGetModel } from '../index'; import * as models from '../index'; describe('index', () => { beforeEach(globalBeforeEach); - describe('getModel()', () => { it('should get model if found', () => { expect(getModel(models.workspace.type)).not.toBeNull(); @@ -24,6 +21,7 @@ describe('index', () => { it('should return null if model not found', () => { const func = () => mustGetModel('UNKNOWN'); + expect(func).toThrowError('The model type UNKNOWN must exist but could not be found.'); }); }); diff --git a/packages/insomnia-app/app/models/__tests__/proto-file.test.js b/packages/insomnia-app/app/models/__tests__/proto-file.test.ts similarity index 89% rename from packages/insomnia-app/app/models/__tests__/proto-file.test.js rename to packages/insomnia-app/app/models/__tests__/proto-file.test.ts index ed659e91b0..c31be2f54a 100644 --- a/packages/insomnia-app/app/models/__tests__/proto-file.test.js +++ b/packages/insomnia-app/app/models/__tests__/proto-file.test.ts @@ -3,6 +3,7 @@ import { globalBeforeEach } from '../../__jest__/before-each'; describe('init()', () => { beforeEach(globalBeforeEach); + it('contains all required fields', async () => { expect(models.protoFile.init()).toEqual({ name: 'New Proto File', @@ -13,9 +14,9 @@ describe('init()', () => { describe('create()', () => { beforeEach(globalBeforeEach); + it('creates a valid protofile', async () => { Date.now = jest.fn().mockReturnValue(1478795580200); - const request = await models.protoFile.create({ name: 'My File', parentId: 'fld_124', @@ -30,15 +31,16 @@ describe('create()', () => { name: 'My File', protoText: 'some proto text', }; - expect(request).toEqual(expected); expect(await models.protoFile.getById(expected._id)).toEqual(expected); }); it('fails when missing parentId', async () => { Date.now = jest.fn().mockReturnValue(1478795580200); - expect(() => models.protoFile.create({ name: 'no parentId' })).toThrow( - 'New ProtoFile missing `parentId`', - ); + expect(() => + models.protoFile.create({ + name: 'no parentId', + }), + ).toThrow('New ProtoFile missing `parentId`'); }); }); diff --git a/packages/insomnia-app/app/models/__tests__/request-meta.test.js b/packages/insomnia-app/app/models/__tests__/request-meta.test.ts similarity index 63% rename from packages/insomnia-app/app/models/__tests__/request-meta.test.js rename to packages/insomnia-app/app/models/__tests__/request-meta.test.ts index ee70858def..34039a0bf8 100644 --- a/packages/insomnia-app/app/models/__tests__/request-meta.test.js +++ b/packages/insomnia-app/app/models/__tests__/request-meta.test.ts @@ -5,12 +5,12 @@ describe('create()', () => { beforeEach(globalBeforeEach); it('fails when missing parentId', async () => { - expect(() => models.requestMeta.create({ pinned: true })).toThrow( - 'New RequestMeta missing `parentId`', - ); - }); - - // it('fails when parentId prefix is not that of a Request', async () => { + expect(() => + models.requestMeta.create({ + pinned: true, + }), + ).toThrow('New RequestMeta missing `parentId`'); + }); // it('fails when parentId prefix is not that of a Request', async () => { // expect(() => models.requestMeta.create({ parentId: 'greq_123' })).toThrow( // 'Expected the parent of RequestMeta to be a Request', // ); diff --git a/packages/insomnia-app/app/models/__tests__/request.test.js b/packages/insomnia-app/app/models/__tests__/request.test.ts similarity index 73% rename from packages/insomnia-app/app/models/__tests__/request.test.js rename to packages/insomnia-app/app/models/__tests__/request.test.ts index 73f549e31d..3cc2573a0f 100644 --- a/packages/insomnia-app/app/models/__tests__/request.test.js +++ b/packages/insomnia-app/app/models/__tests__/request.test.ts @@ -5,6 +5,7 @@ import { CONTENT_TYPE_GRAPHQL } from '../../common/constants'; describe('init()', () => { beforeEach(globalBeforeEach); + it('contains all required fields', async () => { Date.now = jest.fn().mockReturnValue(1478795580200); expect(models.request.init()).toEqual({ @@ -30,9 +31,9 @@ describe('init()', () => { describe('create()', () => { beforeEach(globalBeforeEach); + it('creates a valid request', async () => { Date.now = jest.fn().mockReturnValue(1478795580200); - const request = await models.request.create({ name: 'Test Request', parentId: 'fld_124', @@ -61,30 +62,36 @@ describe('create()', () => { settingRebuildPath: true, settingFollowRedirects: 'global', }; - expect(request).toEqual(expected); expect(await models.request.getById(expected._id)).toEqual(expected); }); it('fails when missing parentId', async () => { Date.now = jest.fn().mockReturnValue(1478795580200); - expect(() => models.request.create({ name: 'Test Request' })).toThrow( - 'New Requests missing `parentId`', - ); + expect(() => + models.request.create({ + name: 'Test Request', + }), + ).toThrow('New Requests missing `parentId`'); }); }); describe('updateMimeType()', () => { beforeEach(globalBeforeEach); + it('adds header when does not exist', async () => { const request = await models.request.create({ name: 'My Request', parentId: 'fld_1', }); expect(request).not.toBeNull(); - const newRequest = await models.request.updateMimeType(request, 'text/html'); - expect(newRequest.headers).toEqual([{ name: 'Content-Type', value: 'text/html' }]); + expect(newRequest.headers).toEqual([ + { + name: 'Content-Type', + value: 'text/html', + }, + ]); }); it('replaces header when exists', async () => { @@ -92,19 +99,34 @@ describe('updateMimeType()', () => { name: 'My Request', parentId: 'fld_1', headers: [ - { name: 'content-tYPE', value: 'application/json' }, - { name: 'foo', value: 'bar' }, - { bad: true }, + { + name: 'content-tYPE', + value: 'application/json', + }, + { + name: 'foo', + value: 'bar', + }, + { + bad: true, + }, null, ], }); expect(request).not.toBeNull(); - const newRequest = await models.request.updateMimeType(request, 'text/html'); expect(newRequest.headers).toEqual([ - { name: 'content-tYPE', value: 'text/html' }, - { name: 'foo', value: 'bar' }, - { bad: true }, + { + name: 'content-tYPE', + value: 'text/html', + }, + { + name: 'foo', + value: 'bar', + }, + { + bad: true, + }, null, ]); }); @@ -113,22 +135,35 @@ describe('updateMimeType()', () => { const request = await models.request.create({ name: 'My Request', parentId: 'fld_1', - headers: [{ name: 'content-tYPE', value: 'application/json' }], + headers: [ + { + name: 'content-tYPE', + value: 'application/json', + }, + ], }); expect(request).not.toBeNull(); - const newRequest = await models.request.updateMimeType(request, 'text/html'); - expect(newRequest.headers).toEqual([{ name: 'content-tYPE', value: 'text/html' }]); + expect(newRequest.headers).toEqual([ + { + name: 'content-tYPE', + value: 'text/html', + }, + ]); }); it('removes existing content-type when set to null (i.e. no body)', async () => { const request = await models.request.create({ name: 'My Request', parentId: 'fld_1', - headers: [{ name: 'content-tYPE', value: 'application/json' }], + headers: [ + { + name: 'content-tYPE', + value: 'application/json', + }, + ], }); expect(request).not.toBeNull(); - const newRequest = await models.request.updateMimeType(request, null); expect(newRequest.body).toEqual({}); expect(newRequest.headers).toEqual([]); @@ -143,7 +178,6 @@ describe('updateMimeType()', () => { }, }); expect(request).not.toBeNull(); - const newRequest = await models.request.updateMimeType(request, 'application/json', false, { text: 'Saved Data', }); @@ -159,7 +193,6 @@ describe('updateMimeType()', () => { }, }); expect(request).not.toBeNull(); - const newRequest = await models.request.updateMimeType(request, 'application/json', false, {}); expect(newRequest.body.text).toEqual('My Data'); }); @@ -167,39 +200,55 @@ describe('updateMimeType()', () => { describe('migrate()', () => { beforeEach(globalBeforeEach); + it('migrates basic case', () => { const original = { headers: [], body: 'hello world!', }; - const expected = { headers: [], - body: { mimeType: '', text: 'hello world!' }, + body: { + mimeType: '', + text: 'hello world!', + }, url: '', }; - expect(models.request.migrate(original)).toEqual(expected); }); it('migrates form-urlencoded', () => { const original = { - headers: [{ name: 'content-type', value: 'application/x-www-form-urlencoded' }], + headers: [ + { + name: 'content-type', + value: 'application/x-www-form-urlencoded', + }, + ], body: 'foo=bar&baz={{ hello }}', }; - const expected = { - headers: [{ name: 'content-type', value: 'application/x-www-form-urlencoded' }], + headers: [ + { + name: 'content-type', + value: 'application/x-www-form-urlencoded', + }, + ], body: { mimeType: 'application/x-www-form-urlencoded', params: [ - { name: 'foo', value: 'bar' }, - { name: 'baz', value: '{{ hello }}' }, + { + name: 'foo', + value: 'bar', + }, + { + name: 'baz', + value: '{{ hello }}', + }, ], }, url: '', }; - expect(models.request.migrate(original)).toEqual(expected); }); @@ -213,7 +262,6 @@ describe('migrate()', () => { ], body: 'foo=bar&baz={{ hello }}', }; - const expected = { headers: [ { @@ -224,31 +272,49 @@ describe('migrate()', () => { body: { mimeType: 'application/x-www-form-urlencoded', params: [ - { name: 'foo', value: 'bar' }, - { name: 'baz', value: '{{ hello }}' }, + { + name: 'foo', + value: 'bar', + }, + { + name: 'baz', + value: '{{ hello }}', + }, ], }, url: '', }; - expect(models.request.migrate(original)).toEqual(expected); }); it('migrates form-urlencoded malformed', () => { const original = { - headers: [{ name: 'content-type', value: 'application/x-www-form-urlencoded' }], + headers: [ + { + name: 'content-type', + value: 'application/x-www-form-urlencoded', + }, + ], body: '{"foo": "bar"}', }; - const expected = { - headers: [{ name: 'content-type', value: 'application/x-www-form-urlencoded' }], + headers: [ + { + name: 'content-type', + value: 'application/x-www-form-urlencoded', + }, + ], body: { mimeType: 'application/x-www-form-urlencoded', - params: [{ name: '{"foo": "bar"}', value: '' }], + params: [ + { + name: '{"foo": "bar"}', + value: '', + }, + ], }, url: '', }; - expect(models.request.migrate(original)).toEqual(expected); }); @@ -261,34 +327,55 @@ describe('migrate()', () => { for (const contentType of Object.keys(contentToMimeMap)) { const original = { - headers: [{ name: 'content-type', value: contentType }], + headers: [ + { + name: 'content-type', + value: contentType, + }, + ], body: '', }; - const expected = { - headers: [{ name: 'content-type', value: contentType }], - body: { mimeType: contentToMimeMap[contentType], text: '' }, + headers: [ + { + name: 'content-type', + value: contentType, + }, + ], + body: { + mimeType: contentToMimeMap[contentType], + text: '', + }, url: '', }; - expect(models.request.migrate(original)).toEqual(expected); } }); it('skips migrate for schema 1', () => { const original = { - body: { mimeType: 'text/plain', text: 'foo' }, + body: { + mimeType: 'text/plain', + text: 'foo', + }, }; - expect(models.request.migrate(original)).toBe(original); }); it('migrates with weird data', () => { - const newBody = { body: { mimeType: '', text: 'foo bar!' } }; - const stringBody = { body: 'foo bar!' }; - const nullBody = { body: null }; + const newBody = { + body: { + mimeType: '', + text: 'foo bar!', + }, + }; + const stringBody = { + body: 'foo bar!', + }; + const nullBody = { + body: null, + }; const noBody = {}; - const expected = { body: { mimeType: '', @@ -296,12 +383,10 @@ describe('migrate()', () => { }, url: '', }; - const expected2 = { body: {}, url: '', }; - expect(models.request.migrate(newBody)).toEqual(expected); expect(models.request.migrate(stringBody)).toEqual(expected); expect(models.request.migrate(nullBody)).toEqual(expected2); @@ -310,13 +395,11 @@ describe('migrate()', () => { it('migrates from initModel()', async () => { Date.now = jest.fn().mockReturnValue(1478795580200); - const original = { _id: 'req_123', headers: [], body: 'hello world!', }; - const expected = { _id: 'req_123', isPrivate: false, @@ -332,7 +415,10 @@ describe('migrate()', () => { authentication: {}, parameters: [], parentId: null, - body: { mimeType: '', text: 'hello world!' }, + body: { + mimeType: '', + text: 'hello world!', + }, settingStoreCookies: true, settingSendCookies: true, settingDisableRenderRequestBody: false, @@ -340,7 +426,6 @@ describe('migrate()', () => { settingRebuildPath: true, settingFollowRedirects: 'global', }; - const migrated = await models.initModel(models.request.type, original); expect(migrated).toEqual(expected); }); @@ -352,21 +437,29 @@ describe('newBodyGraphQL()', () => { '{"query": "query getCustomer() {\\\\n id\\\\n name\\\\n email\\\\n __typename\\\\n }\\\\n"}'; const expectedTextOutput = '{"query": "query getCustomer() { id name email __typename }"}'; const actualOutput = newBodyGraphQL(input); - - expect(actualOutput).toEqual({ mimeType: CONTENT_TYPE_GRAPHQL, text: expectedTextOutput }); + expect(actualOutput).toEqual({ + mimeType: CONTENT_TYPE_GRAPHQL, + text: expectedTextOutput, + }); }); it('does nothing to empty string', () => { const input = ''; const expectedTextOutput = ''; const actualOutput = newBodyGraphQL(input); - expect(actualOutput).toEqual({ mimeType: CONTENT_TYPE_GRAPHQL, text: expectedTextOutput }); + expect(actualOutput).toEqual({ + mimeType: CONTENT_TYPE_GRAPHQL, + text: expectedTextOutput, + }); }); it('does nothing to object that has no \\\\n characters', () => { const input = '{ "foo": "bar" }'; const expectedTextOutput = '{ "foo": "bar" }'; const actualOutput = newBodyGraphQL(input); - expect(actualOutput).toEqual({ mimeType: CONTENT_TYPE_GRAPHQL, text: expectedTextOutput }); + expect(actualOutput).toEqual({ + mimeType: CONTENT_TYPE_GRAPHQL, + text: expectedTextOutput, + }); }); }); diff --git a/packages/insomnia-app/app/models/__tests__/response.test.js b/packages/insomnia-app/app/models/__tests__/response.test.ts similarity index 94% rename from packages/insomnia-app/app/models/__tests__/response.test.js rename to packages/insomnia-app/app/models/__tests__/response.test.ts index f25c436d47..fcdc5c450e 100644 --- a/packages/insomnia-app/app/models/__tests__/response.test.js +++ b/packages/insomnia-app/app/models/__tests__/response.test.ts @@ -3,7 +3,7 @@ import zlib from 'zlib'; import fs from 'fs'; import * as models from '../../models'; import { globalBeforeEach } from '../../__jest__/before-each'; -import { getDataDirectory } from '../../common/misc'; +import { getDataDirectory } from '../../common/electron-helpers'; describe('migrate()', () => { beforeEach(async () => { @@ -18,27 +18,29 @@ describe('migrate()', () => { }); it('migrates utf8 body correctly', async () => { - const initialModel = { body: 'hello world!', encoding: 'utf8' }; - + const initialModel = { + body: 'hello world!', + encoding: 'utf8', + }; const newModel = await models.initModel(models.response.type, initialModel); const expectedBodyPath = path.join( getDataDirectory(), 'responses/fc3ff98e8c6a0d3087d515c0473f8677.zip', ); const storedBody = models.response.getBodyBuffer(newModel); - // Should have set bodyPath and stored the body expect(newModel.bodyPath).toBe(expectedBodyPath); expect(storedBody.toString()).toBe('hello world!'); - // Should have stripped these expect(newModel.body).toBeUndefined(); expect(newModel.encoding).toBeUndefined(); }); it('migrates base64 body correctly', async () => { - const initialModel = { body: 'aGVsbG8gd29ybGQh', encoding: 'base64' }; - + const initialModel = { + body: 'aGVsbG8gd29ybGQh', + encoding: 'base64', + }; const newModel = await models.initModel(models.response.type, initialModel); jest.runAllTimers(); const expectedBodyPath = path.join( @@ -46,47 +48,42 @@ describe('migrate()', () => { 'responses/fc3ff98e8c6a0d3087d515c0473f8677.zip', ); const storedBody = models.response.getBodyBuffer(newModel); - // Should have stripped these expect(newModel.body).toBeUndefined(); expect(newModel.encoding).toBeUndefined(); - // Should have set bodyPath and stored the body expect(newModel.bodyPath).toBe(expectedBodyPath); expect(storedBody.toString()).toBe('hello world!'); }); it('migrates empty body', async () => { - const initialModel = { body: '' }; - + const initialModel = { + body: '', + }; const newModel = await models.initModel(models.response.type, initialModel); jest.runAllTimers(); jest.runAllTimers(); - const expectedBodyPath = path.join( getDataDirectory(), 'responses/d41d8cd98f00b204e9800998ecf8427e.zip', ); const storedBody = models.response.getBodyBuffer(newModel); - // Should have stripped these expect(newModel.body).toBeUndefined(); expect(newModel.encoding).toBeUndefined(); - // Should have set bodyPath and stored the body expect(newModel.bodyPath).toBe(expectedBodyPath); expect(storedBody.toString()).toBe(''); }); it('does not migrate body again', async () => { - const initialModel = { bodyPath: '/foo/bar' }; - + const initialModel = { + bodyPath: '/foo/bar', + }; const newModel = await models.initModel(models.response.type, initialModel); - // Should have stripped these expect(newModel.body).toBeUndefined(); expect(newModel.encoding).toBeUndefined(); - // Should have set bodyPath and stored the body expect(newModel.bodyPath).toBe('/foo/bar'); }); @@ -94,10 +91,10 @@ describe('migrate()', () => { it('does it', async () => { const bodyPath = path.join(getDataDirectory(), 'foo.zip'); fs.writeFileSync(bodyPath, zlib.gzipSync('Hello World!')); - - const response = await models.initModel(models.response.type, { bodyPath }); + const response = await models.initModel(models.response.type, { + bodyPath, + }); const body = await models.response.getBodyBuffer(response).toString(); - expect(response.bodyCompression).toBe('zip'); expect(body).toBe('Hello World!'); }); @@ -108,7 +105,6 @@ describe('migrate()', () => { encoding: 'base64', }); const body = await models.response.getBodyBuffer(response).toString(); - expect(response.bodyCompression).toBe(null); expect(body).toBe('hello world!'); }); @@ -157,9 +153,7 @@ describe('cleanDeletedResponses()', function() { const mockUnlinkSync = jest.spyOn(fs, 'unlinkSync'); mockReaddirSync.mockReturnValueOnce([]); mockUnlinkSync.mockImplementation(); - await models.response.cleanDeletedResponses(); - expect(fs.unlinkSync.mock.calls.length).toBe(0); }); @@ -167,6 +161,7 @@ describe('cleanDeletedResponses()', function() { const responsesDir = path.join(getDataDirectory(), 'responses'); const dbResponseIds = await createModels(responsesDir, 10); const notDbResponseIds = []; + for (let index = 100; index < 110; index++) { notDbResponseIds.push('res_' + index); } @@ -175,9 +170,7 @@ describe('cleanDeletedResponses()', function() { const mockUnlinkSync = jest.spyOn(fs, 'unlinkSync'); mockReaddirSync.mockReturnValueOnce([...dbResponseIds, ...notDbResponseIds]); mockUnlinkSync.mockImplementation(); - await models.response.cleanDeletedResponses(); - expect(fs.unlinkSync.mock.calls.length).toBe(notDbResponseIds.length); Object.keys(notDbResponseIds).map(index => { const resId = notDbResponseIds[index]; @@ -204,7 +197,6 @@ async function createModels(responsesDir, count) { const workspaceId = 'wrk_' + index; const requestId = 'req_' + index; const responseId = 'res_' + index; - await models.workspace.create({ _id: workspaceId, created: 111, @@ -218,7 +210,6 @@ async function createModels(responsesDir, count) { metaSortKey: 0, url: 'https://insomnia.rest', }); - await models.response.create({ _id: responseId, parentId: requestId, diff --git a/packages/insomnia-app/app/models/__tests__/workspace.test.js b/packages/insomnia-app/app/models/__tests__/workspace.test.ts similarity index 78% rename from packages/insomnia-app/app/models/__tests__/workspace.test.js rename to packages/insomnia-app/app/models/__tests__/workspace.test.ts index fec776bbce..3ed54046c1 100644 --- a/packages/insomnia-app/app/models/__tests__/workspace.test.js +++ b/packages/insomnia-app/app/models/__tests__/workspace.test.ts @@ -4,15 +4,21 @@ import { WorkspaceScopeKeys } from '../workspace'; describe('migrate()', () => { beforeEach(globalBeforeEach); + it('migrates client certificates properly', async () => { const workspace = await models.workspace.create({ name: 'My Workspace', certificates: [ - { key: 'key', passphrase: 'mypass' }, - { disabled: true, cert: 'cert' }, + { + key: 'key', + passphrase: 'mypass', + }, + { + disabled: true, + cert: 'cert', + }, ], }); - const migratedWorkspace = await models.workspace.migrate(workspace); const certs = await models.clientCertificate.findByParentId(workspace._id); @@ -51,9 +57,7 @@ describe('migrate()', () => { type: 'ClientCertificate', }, ]); - expect(migratedWorkspace.certificates).toBeUndefined(); - // Make sure we don't create new certs if we migrate again await models.workspace.migrate(migratedWorkspace); const certsAgain = await models.clientCertificate.findByParentId(workspace._id); @@ -61,10 +65,11 @@ describe('migrate()', () => { }); it('creates api spec for workspace id', async () => { - const workspace = await models.workspace.create({ name: 'My Workspace' }); + const workspace = await models.workspace.create({ + name: 'My Workspace', + }); await models.apiSpec.removeWhere(workspace._id); expect(await models.apiSpec.getByParentId(workspace._id)).toBe(null); - await models.workspace.migrate(workspace); const spec = await models.apiSpec.getByParentId(workspace._id); expect(spec).not.toBe(null); @@ -72,18 +77,28 @@ describe('migrate()', () => { }); it('translates the scope correctly', async () => { - const specW = await models.workspace.create({ scope: 'spec' }); - const debugW = await models.workspace.create({ scope: 'debug' }); - const nullW = await models.workspace.create({ scope: null }); - const somethingElseW = await models.workspace.create({ scope: 'something' }); - const designW = await models.workspace.create({ scope: WorkspaceScopeKeys.design }); - const collectionW = await models.workspace.create({ scope: WorkspaceScopeKeys.collection }); - + const specW = await models.workspace.create({ + scope: 'spec', + }); + const debugW = await models.workspace.create({ + scope: 'debug', + }); + const nullW = await models.workspace.create({ + scope: null, + }); + const somethingElseW = await models.workspace.create({ + scope: 'something', + }); + const designW = await models.workspace.create({ + scope: WorkspaceScopeKeys.design, + }); + const collectionW = await models.workspace.create({ + scope: WorkspaceScopeKeys.collection, + }); await models.workspace.migrate(specW); await models.workspace.migrate(debugW); await models.workspace.migrate(nullW); await models.workspace.migrate(somethingElseW); - expect(specW.scope).toBe(WorkspaceScopeKeys.design); expect(debugW.scope).toBe(WorkspaceScopeKeys.collection); expect(nullW.scope).toBe(WorkspaceScopeKeys.collection); diff --git a/packages/insomnia-app/app/models/api-spec.js b/packages/insomnia-app/app/models/api-spec.ts similarity index 50% rename from packages/insomnia-app/app/models/api-spec.js rename to packages/insomnia-app/app/models/api-spec.ts index 6945d24668..ec1c13bb91 100644 --- a/packages/insomnia-app/app/models/api-spec.js +++ b/packages/insomnia-app/app/models/api-spec.ts @@ -1,19 +1,22 @@ -// @flow import type { BaseModel } from './index'; -import * as db from '../common/database'; +import { database as db } from '../common/database'; import { strings } from '../common/strings'; export const name = 'ApiSpec'; + export const type = 'ApiSpec'; + export const prefix = 'spc'; + export const canDuplicate = true; + export const canSync = false; -type BaseApiSpec = { - fileName: string, - contentType: 'json' | 'yaml', - contents: string, -}; +interface BaseApiSpec { + fileName: string; + contentType: 'json' | 'yaml'; + contents: string; +} export type ApiSpec = BaseModel & BaseApiSpec; @@ -25,22 +28,24 @@ export function init(): BaseApiSpec { }; } -export async function migrate(doc: ApiSpec): Promise { +export async function migrate(doc: ApiSpec) { return doc; } -export function getByParentId(workspaceId: string): Promise { - return db.getWhere(type, { parentId: workspaceId }); +export function getByParentId(workspaceId: string) { + return db.getWhere(type, { parentId: workspaceId }); } export async function getOrCreateForParentId( workspaceId: string, - patch: $Shape = {}, -): Promise { - const spec = await db.getWhere(type, { parentId: workspaceId }); + patch: Partial = {}, +) { + const spec = await db.getWhere(type, { + parentId: workspaceId, + }); if (!spec) { - return db.docCreate(type, { ...patch, parentId: workspaceId }); + return db.docCreate(type, { ...patch, parentId: workspaceId }); } return spec; @@ -48,20 +53,20 @@ export async function getOrCreateForParentId( export async function updateOrCreateForParentId( workspaceId: string, - patch: $Shape = {}, -): Promise { + patch: Partial = {}, +) { const spec = await getOrCreateForParentId(workspaceId); return db.docUpdate(spec, patch); } -export async function all(): Promise> { - return db.all(type); +export async function all() { + return db.all(type); } -export function update(apiSpec: ApiSpec, patch: $Shape = {}): Promise { +export function update(apiSpec: ApiSpec, patch: Partial = {}) { return db.docUpdate(apiSpec, patch); } -export function removeWhere(parentId: string): Promise { +export function removeWhere(parentId: string) { return db.removeWhere(type, { parentId }); } diff --git a/packages/insomnia-app/app/models/client-certificate.js b/packages/insomnia-app/app/models/client-certificate.js deleted file mode 100644 index b06e32d576..0000000000 --- a/packages/insomnia-app/app/models/client-certificate.js +++ /dev/null @@ -1,72 +0,0 @@ -// @flow -import * as db from '../common/database'; -import type { BaseModel } from './index'; - -export const name = 'Client Certificate'; -export const type = 'ClientCertificate'; -export const prefix = 'crt'; -export const canDuplicate = true; -export const canSync = false; - -type BaseClientCertificate = { - parentId: string, - host: string, - passphrase: string | null, - cert: string | null, - key: string | null, - pfx: string | null, - disabled: boolean, - - // For sync control - isPrivate: boolean, -}; - -export type ClientCertificate = BaseModel & BaseClientCertificate; - -export function init(): BaseClientCertificate { - return { - parentId: '', - host: '', - passphrase: null, - disabled: false, - cert: null, - key: null, - pfx: null, - isPrivate: false, - }; -} - -export async function migrate(doc: ClientCertificate) { - return doc; -} - -export function create(patch: $Shape = {}): Promise { - if (!patch.parentId) { - throw new Error('New ClientCertificate missing `parentId`: ' + JSON.stringify(patch)); - } - - return db.docCreate(type, patch); -} - -export function update( - cert: ClientCertificate, - patch: $Shape = {}, -): Promise { - return db.docUpdate(cert, patch); -} - -export function getById(id: string): Promise { - return db.get(type, id); -} - -export function findByParentId(parentId: string): Promise> { - return db.find(type, { parentId }); -} - -export function remove(cert: ClientCertificate): Promise { - return db.remove(cert); -} - -export function all(): Promise> { - return db.all(type); -} diff --git a/packages/insomnia-app/app/models/client-certificate.ts b/packages/insomnia-app/app/models/client-certificate.ts new file mode 100644 index 0000000000..0282fa6a60 --- /dev/null +++ b/packages/insomnia-app/app/models/client-certificate.ts @@ -0,0 +1,74 @@ +import { database as db } from '../common/database'; +import type { BaseModel } from './index'; + +export const name = 'Client Certificate'; + +export const type = 'ClientCertificate'; + +export const prefix = 'crt'; + +export const canDuplicate = true; + +export const canSync = false; + +interface BaseClientCertificate { + parentId: string; + host: string; + passphrase: string | null; + cert: string | null; + key: string | null; + pfx: string | null; + disabled: boolean; + // For sync control + isPrivate: boolean; +} + +export type ClientCertificate = BaseModel & BaseClientCertificate; + +export function init(): BaseClientCertificate { + return { + parentId: '', + host: '', + passphrase: null, + disabled: false, + cert: null, + key: null, + pfx: null, + isPrivate: false, + }; +} + +export async function migrate(doc: ClientCertificate) { + return doc; +} + +export function create(patch: Partial = {}) { + if (!patch.parentId) { + throw new Error('New ClientCertificate missing `parentId`: ' + JSON.stringify(patch)); + } + + return db.docCreate(type, patch); +} + +export function update( + cert: ClientCertificate, + patch: Partial = {}, +) { + return db.docUpdate(cert, patch); +} + +export function getById(id: string) { + return db.get(type, id); +} + +export function findByParentId(parentId: string) { + return db.find(type, { parentId }); +} + +export function remove(cert: ClientCertificate) { + return db.remove(cert); +} + +export function all() { + return db.all(type); +} diff --git a/packages/insomnia-app/app/models/cookie-jar.js b/packages/insomnia-app/app/models/cookie-jar.ts similarity index 53% rename from packages/insomnia-app/app/models/cookie-jar.js rename to packages/insomnia-app/app/models/cookie-jar.ts index d63047f3bf..d45a6618ef 100644 --- a/packages/insomnia-app/app/models/cookie-jar.js +++ b/packages/insomnia-app/app/models/cookie-jar.ts @@ -1,36 +1,38 @@ -// @flow import crypto from 'crypto'; -import * as db from '../common/database'; +import { database as db } from '../common/database'; import type { BaseModel } from './index'; export const name = 'Cookie Jar'; + export const type = 'CookieJar'; + export const prefix = 'jar'; + export const canDuplicate = true; + export const canSync = false; -export type Cookie = { - id: string, - key: string, - value: string, - expires: Date | string | number | null, - domain: string, - path: string, - secure: boolean, - httpOnly: boolean, +export interface Cookie { + id: string; + key: string; + value: string; + expires: Date | string | number | null; + domain: string; + path: string; + secure: boolean; + httpOnly: boolean; + extensions?: Array; + creation?: Date; + creationIndex?: number; + hostOnly?: boolean; + pathIsDefault?: boolean; + lastAccessed?: Date; +} - extensions?: Array, - creation?: Date, - creationIndex?: number, - hostOnly?: boolean, - pathIsDefault?: boolean, - lastAccessed?: Date, -}; - -type BaseCookieJar = { - name: string, - cookies: Array, -}; +interface BaseCookieJar { + name: string; + cookies: Array; +} export type CookieJar = BaseModel & BaseCookieJar; @@ -41,46 +43,43 @@ export function init() { }; } -export function migrate(doc: CookieJar): CookieJar { +export function migrate(doc: CookieJar) { doc = migrateCookieId(doc); return doc; } -export async function create(patch: $Shape) { +export async function create(patch: Partial) { if (!patch.parentId) { throw new Error(`New CookieJar missing \`parentId\`: ${JSON.stringify(patch)}`); } - return db.docCreate(type, patch); + return db.docCreate(type, patch); } export async function getOrCreateForParentId(parentId: string) { - const cookieJars = await db.find(type, { parentId }); + const cookieJars = await db.find(type, { parentId }); + if (cookieJars.length === 0) { return create({ parentId, - // Deterministic ID. It helps reduce sync complexity since we won't have to // de-duplicate environments. - _id: `${prefix}_${crypto - .createHash('sha1') - .update(parentId) - .digest('hex')}`, + _id: `${prefix}_${crypto.createHash('sha1').update(parentId).digest('hex')}`, }); } else { return cookieJars[0]; } } -export async function all(): Promise> { - return db.all(type); +export async function all() { + return db.all(type); } export async function getById(id: string) { return db.get(type, id); } -export async function update(cookieJar: CookieJar, patch: $Shape = {}) { +export async function update(cookieJar: CookieJar, patch: Partial = {}) { return db.docUpdate(cookieJar, patch); } @@ -88,10 +87,9 @@ export async function update(cookieJar: CookieJar, patch: $Shape = {} function migrateCookieId(cookieJar: CookieJar) { for (const cookie of cookieJar.cookies) { if (!cookie.id) { - cookie.id = Math.random() - .toString() - .replace('0.', ''); + cookie.id = Math.random().toString().replace('0.', ''); } } + return cookieJar; } diff --git a/packages/insomnia-app/app/models/environment.js b/packages/insomnia-app/app/models/environment.ts similarity index 55% rename from packages/insomnia-app/app/models/environment.js rename to packages/insomnia-app/app/models/environment.ts index 25f021f861..5f816be489 100644 --- a/packages/insomnia-app/app/models/environment.js +++ b/packages/insomnia-app/app/models/environment.ts @@ -1,25 +1,27 @@ -// @flow import * as crypto from 'crypto'; -import * as db from '../common/database'; +import { database as db } from '../common/database'; import type { BaseModel } from './index'; import type { Workspace } from './workspace'; export const name = 'Environment'; + export const type = 'Environment'; + export const prefix = 'env'; + export const canDuplicate = true; + export const canSync = true; -type BaseEnvironment = { - name: string, - data: Object, - dataPropertyOrder: Object | null, - color: string | null, - metaSortKey: number, - +interface BaseEnvironment { + name: string; + data: Record; + dataPropertyOrder: Record | null; + color: string | null; + metaSortKey: number; // For sync control - isPrivate: boolean, -}; + isPrivate: boolean; +} export type Environment = BaseModel & BaseEnvironment; @@ -34,47 +36,53 @@ export function init() { }; } -export function migrate(doc: Environment): Environment { +export function migrate(doc: Environment) { return doc; } -export function create(patch: $Shape = {}): Promise { +export function create(patch: Partial = {}) { if (!patch.parentId) { throw new Error(`New Environment missing \`parentId\`: ${JSON.stringify(patch)}`); } - return db.docCreate(type, patch); + return db.docCreate(type, patch); } -export function update(environment: Environment, patch: $Shape): Promise { +export function update(environment: Environment, patch: Partial) { return db.docUpdate(environment, patch); } -export function findByParentId(parentId: string): Promise> { - return db.find(type, { parentId }, { metaSortKey: 1 }); +export function findByParentId(parentId: string) { + return db.find( + type, + { + parentId, + }, + { + metaSortKey: 1, + }, + ); } -export async function getOrCreateForWorkspaceId(workspaceId: string): Promise { - const environments = await db.find(type, { parentId: workspaceId }); +export async function getOrCreateForWorkspaceId(workspaceId: string) { + const environments = await db.find(type, { + parentId: workspaceId, + }); if (!environments.length) { return create({ parentId: workspaceId, name: 'Base Environment', - // Deterministic base env ID. It helps reduce sync complexity since we won't have to // de-duplicate environments. - _id: `${prefix}_${crypto - .createHash('sha1') - .update(workspaceId) - .digest('hex')}`, + _id: `${prefix}_${crypto.createHash('sha1').update(workspaceId).digest('hex')}`, }); } return environments[environments.length - 1]; } -export async function getOrCreateForWorkspace(workspace: Workspace): Promise { +export async function getOrCreateForWorkspace(workspace: Workspace) { return getOrCreateForWorkspaceId(workspace._id); } @@ -82,24 +90,29 @@ export function getById(id: string): Promise { return db.get(type, id); } -export async function duplicate(environment: Environment): Promise { +export async function duplicate(environment: Environment) { const name = `${environment.name} (Copy)`; - // Get sort key of next environment - const q = { metaSortKey: { $gt: environment.metaSortKey } }; - const [nextEnvironment] = await db.find(type, q, { metaSortKey: 1 }); + const q = { + metaSortKey: { + $gt: environment.metaSortKey, + }, + }; + // @ts-expect-error -- TSCONVERSION appears to be a genuine error + const [nextEnvironment] = await db.find(type, q, { metaSortKey: 1 }); const nextSortKey = nextEnvironment ? nextEnvironment.metaSortKey : environment.metaSortKey + 100; - // Calculate new sort key const metaSortKey = (environment.metaSortKey + nextSortKey) / 2; - - return db.duplicate(environment, { name, metaSortKey }); + return db.duplicate(environment, { + name, + metaSortKey, + }); } -export function remove(environment: Environment): Promise { +export function remove(environment: Environment) { return db.remove(environment); } -export function all(): Promise> { - return db.all(type); +export function all() { + return db.all(type); } diff --git a/packages/insomnia-app/app/models/git-repository.js b/packages/insomnia-app/app/models/git-repository.js deleted file mode 100644 index d5404e2ed0..0000000000 --- a/packages/insomnia-app/app/models/git-repository.js +++ /dev/null @@ -1,64 +0,0 @@ -// @flow -import type { BaseModel } from './index'; - -import * as db from '../common/database'; -import type { GitCredentials } from '../sync/git/git-vcs'; - -type BaseGitRepository = { - needsFullClone: boolean, - uri: string, - credentials: GitCredentials | null, - author: { - name: string, - email: string, - }, - uriNeedsMigration: boolean, -}; - -export type GitRepository = BaseModel & BaseGitRepository; - -export const name = 'Git Repository'; -export const type = 'GitRepository'; -export const prefix = 'git'; -export const canDuplicate = false; -export const canSync = false; - -export function init(): BaseGitRepository { - return { - needsFullClone: false, - uri: '', - credentials: null, - author: { - name: '', - email: '', - }, - uriNeedsMigration: true, - }; -} - -export function migrate(doc: GitRepository): GitRepository { - return doc; -} - -export function create(patch: $Shape = {}): Promise { - return db.docCreate(type, { - uriNeedsMigration: false, - ...patch, - }); -} - -export async function getById(id: string): Promise { - return db.getWhere(type, { _id: id }); -} - -export function update(repo: GitRepository, patch: $Shape): Promise { - return db.docUpdate(repo, patch); -} - -export function remove(repo: GitRepository): Promise { - return db.remove(repo); -} - -export function all(): Promise> { - return db.all(type); -} diff --git a/packages/insomnia-app/app/models/git-repository.ts b/packages/insomnia-app/app/models/git-repository.ts new file mode 100644 index 0000000000..39703300b0 --- /dev/null +++ b/packages/insomnia-app/app/models/git-repository.ts @@ -0,0 +1,66 @@ +import type { BaseModel } from './index'; +import { database as db } from '../common/database'; +import type { GitCredentials } from '../sync/git/git-vcs'; + +export type GitRepository = BaseModel & BaseGitRepository; + +export const name = 'Git Repository'; + +export const type = 'GitRepository'; + +export const prefix = 'git'; + +export const canDuplicate = false; + +export const canSync = false; + +export function init(): BaseGitRepository { + return { + needsFullClone: false, + uri: '', + credentials: null, + author: { + name: '', + email: '', + }, + uriNeedsMigration: true, + }; +} + +interface BaseGitRepository { + needsFullClone: boolean; + uri: string; + credentials: GitCredentials | null; + author: { + name: string; + email: string; + }; + uriNeedsMigration: boolean; +} + +export function migrate(doc: GitRepository) { + return doc; +} + +export function create(patch: Partial = {}) { + return db.docCreate(type, { + uriNeedsMigration: false, + ...patch, + }); +} + +export async function getById(id: string) { + return db.getWhere(type, { _id: id }); +} + +export function update(repo: GitRepository, patch: Partial) { + return db.docUpdate(repo, patch); +} + +export function remove(repo: GitRepository) { + return db.remove(repo); +} + +export function all() { + return db.all(type); +} diff --git a/packages/insomnia-app/app/models/grpc-request-meta.js b/packages/insomnia-app/app/models/grpc-request-meta.ts similarity index 58% rename from packages/insomnia-app/app/models/grpc-request-meta.js rename to packages/insomnia-app/app/models/grpc-request-meta.ts index 00a230434b..666ce702db 100644 --- a/packages/insomnia-app/app/models/grpc-request-meta.js +++ b/packages/insomnia-app/app/models/grpc-request-meta.ts @@ -1,18 +1,21 @@ -// @flow -import * as db from '../common/database'; +import { database as db } from '../common/database'; import type { BaseModel } from './index'; import { isGrpcRequestId } from './helpers/is-model'; export const name = 'gRPC Request Meta'; + export const type = 'GrpcRequestMeta'; + export const prefix = 'greqm'; + export const canDuplicate = false; + export const canSync = false; -type BaseGrpcRequestMeta = { - pinned: boolean, - lastActive: number, -}; +interface BaseGrpcRequestMeta { + pinned: boolean; + lastActive: number; +} export type GrpcRequestMeta = BaseModel & BaseGrpcRequestMeta; @@ -23,34 +26,30 @@ export function init() { }; } -export function migrate(doc: GrpcRequestMeta): GrpcRequestMeta { +export function migrate(doc: GrpcRequestMeta) { return doc; } -export function create(patch: $Shape = {}): Promise { +export function create(patch: Partial = {}) { if (!patch.parentId) { throw new Error('New GrpcRequestMeta missing `parentId`'); } expectParentToBeGrpcRequest(patch.parentId); - - return db.docCreate(type, patch); + return db.docCreate(type, patch); } -export function update( - requestMeta: GrpcRequestMeta, - patch: $Shape, -): Promise { +export function update(requestMeta: GrpcRequestMeta, patch: Partial) { expectParentToBeGrpcRequest(patch.parentId || requestMeta.parentId); return db.docUpdate(requestMeta, patch); } -export function getByParentId(parentId: string): Promise { +export function getByParentId(parentId: string) { expectParentToBeGrpcRequest(parentId); - return db.getWhere(type, { parentId }); + return db.getWhere(type, { parentId }); } -export async function getOrCreateByParentId(parentId: string): Promise { +export async function getOrCreateByParentId(parentId: string) { const requestMeta = await getByParentId(parentId); if (requestMeta) { @@ -60,22 +59,27 @@ export async function getOrCreateByParentId(parentId: string): Promise) { +export async function updateOrCreateByParentId(parentId: string, patch: Partial) { const requestMeta = await getByParentId(parentId); if (requestMeta) { return update(requestMeta, patch); } else { - const newPatch = Object.assign({ parentId }, patch); + const newPatch = Object.assign( + { + parentId, + }, + patch, + ); return create(newPatch); } } -export function all(): Promise> { - return db.all(type); +export function all() { + return db.all(type); } -function expectParentToBeGrpcRequest(parentId: string) { +function expectParentToBeGrpcRequest(parentId: string | null) { if (!isGrpcRequestId(parentId)) { throw new Error('Expected the parent of GrpcRequestMeta to be a GrpcRequest'); } diff --git a/packages/insomnia-app/app/models/grpc-request.js b/packages/insomnia-app/app/models/grpc-request.js deleted file mode 100644 index 1082a1aa6b..0000000000 --- a/packages/insomnia-app/app/models/grpc-request.js +++ /dev/null @@ -1,102 +0,0 @@ -// @flow -import * as db from '../common/database'; -import type { BaseModel } from './index'; - -export const name = 'gRPC Request'; -export const type = 'GrpcRequest'; -export const prefix = 'greq'; -export const canDuplicate = true; -export const canSync = true; - -export type GrpcRequestBody = { - text?: string, -}; - -type BaseGrpcRequest = { - name: string, - url: string, - description: string, - protoFileId?: string, - protoMethodName?: string, - body: GrpcRequestBody, - metaSortKey: number, - isPrivate: boolean, -}; - -export type GrpcRequest = BaseModel & BaseGrpcRequest; - -export function init(): BaseGrpcRequest { - return { - url: '', - name: 'New gRPC Request', - description: '', - protoFileId: '', - protoMethodName: '', - body: { - text: '{}', - }, - metaSortKey: -1 * Date.now(), - idPrivate: false, - }; -} - -export function migrate(doc: GrpcRequest): GrpcRequest { - return doc; -} - -export function create(patch: $Shape = {}): Promise { - if (!patch.parentId) { - throw new Error('New GrpcRequest missing `parentId`'); - } - - return db.docCreate(type, patch); -} - -export function remove(obj: GrpcRequest): Promise { - return db.remove(obj); -} - -export function update(obj: GrpcRequest, patch: $Shape = {}): Promise { - return db.docUpdate(obj, patch); -} - -export function getById(_id: string): Promise { - return db.getWhere(type, { _id }); -} - -export function findByProtoFileId(protoFileId: string): Promise> { - return db.find(type, { protoFileId }); -} - -export function findByParentId(parentId: string): Promise> { - return db.find(type, { parentId }); -} - -// This is duplicated (lol) from models/request.js -export async function duplicate( - request: GrpcRequest, - patch: $Shape = {}, -): Promise { - // Only set name and "(Copy)" if the patch does - // not define it and the request itself has a name. - // Otherwise leave it blank so the request URL can - // fill it in automatically. - if (!patch.name && request.name) { - patch.name = `${request.name} (Copy)`; - } - - // Get sort key of next request - const q = { metaSortKey: { $gt: request.metaSortKey } }; - const [nextRequest] = await db.find(type, q, { metaSortKey: 1 }); - const nextSortKey = nextRequest ? nextRequest.metaSortKey : request.metaSortKey + 100; - - // Calculate new sort key - const sortKeyIncrement = (nextSortKey - request.metaSortKey) / 2; - const metaSortKey = request.metaSortKey + sortKeyIncrement; - - return db.duplicate(request, { name, metaSortKey, ...patch }); -} - -export function all(): Promise> { - return db.all(type); -} diff --git a/packages/insomnia-app/app/models/grpc-request.ts b/packages/insomnia-app/app/models/grpc-request.ts new file mode 100644 index 0000000000..ec6726ae05 --- /dev/null +++ b/packages/insomnia-app/app/models/grpc-request.ts @@ -0,0 +1,111 @@ +import { database as db } from '../common/database'; +import type { BaseModel } from './index'; + +export const name = 'gRPC Request'; + +export const type = 'GrpcRequest'; + +export const prefix = 'greq'; + +export const canDuplicate = true; + +export const canSync = true; + +export interface GrpcRequestBody { + text?: string; +} + +interface BaseGrpcRequest { + name: string; + url: string; + description: string; + protoFileId?: string; + protoMethodName?: string; + body: GrpcRequestBody; + metaSortKey: number; + isPrivate: boolean; +} + +export type GrpcRequest = BaseModel & BaseGrpcRequest; + +export function init(): BaseGrpcRequest { + return { + url: '', + name: 'New gRPC Request', + description: '', + protoFileId: '', + protoMethodName: '', + body: { + text: '{}', + }, + metaSortKey: -1 * Date.now(), + isPrivate: false, + }; +} + +export function migrate(doc: GrpcRequest) { + return doc; +} + +export function create(patch: Partial = {}) { + if (!patch.parentId) { + throw new Error('New GrpcRequest missing `parentId`'); + } + + return db.docCreate(type, patch); +} + +export function remove(obj: GrpcRequest) { + return db.remove(obj); +} + +export function update(obj: GrpcRequest, patch: Partial = {}) { + return db.docUpdate(obj, patch); +} + +export function getById(_id: string) { + return db.getWhere(type, { _id }); +} + +export function findByProtoFileId(protoFileId: string) { + return db.find(type, { protoFileId }); +} + +export function findByParentId(parentId: string) { + return db.find(type, { parentId }); +} + +// This is duplicated (lol) from models/request.js +export async function duplicate(request: GrpcRequest, patch: Partial = {}) { + // Only set name and "(Copy)" if the patch does + // not define it and the request itself has a name. + // Otherwise leave it blank so the request URL can + // fill it in automatically. + if (!patch.name && request.name) { + patch.name = `${request.name} (Copy)`; + } + + // Get sort key of next request + const q = { + metaSortKey: { + $gt: request.metaSortKey, + }, + }; + // @ts-expect-error -- TSCONVERSION + const [nextRequest] = await db.find(type, q, { + metaSortKey: 1, + }); + const nextSortKey = nextRequest ? nextRequest.metaSortKey : request.metaSortKey + 100; + // Calculate new sort key + const sortKeyIncrement = (nextSortKey - request.metaSortKey) / 2; + const metaSortKey = request.metaSortKey + sortKeyIncrement; + return db.duplicate(request, { + name, + metaSortKey, + ...patch, + }); +} + +export function all() { + return db.all(type); +} diff --git a/packages/insomnia-app/app/models/helpers/__tests__/get-workspace-name.test.js b/packages/insomnia-app/app/models/helpers/__tests__/get-workspace-name.test.ts similarity index 98% rename from packages/insomnia-app/app/models/helpers/__tests__/get-workspace-name.test.js rename to packages/insomnia-app/app/models/helpers/__tests__/get-workspace-name.test.ts index 36a945847b..7a7aac230c 100644 --- a/packages/insomnia-app/app/models/helpers/__tests__/get-workspace-name.test.js +++ b/packages/insomnia-app/app/models/helpers/__tests__/get-workspace-name.test.ts @@ -1,5 +1,3 @@ -// @flow - import * as models from '../../../models'; import getWorkspaceName from '../get-workspace-name'; import { WorkspaceScopeKeys } from '../../workspace'; diff --git a/packages/insomnia-app/app/models/helpers/__tests__/git-repository-operations.test.js b/packages/insomnia-app/app/models/helpers/__tests__/git-repository-operations.test.ts similarity index 94% rename from packages/insomnia-app/app/models/helpers/__tests__/git-repository-operations.test.js rename to packages/insomnia-app/app/models/helpers/__tests__/git-repository-operations.test.ts index e12a870047..b47ac30a15 100644 --- a/packages/insomnia-app/app/models/helpers/__tests__/git-repository-operations.test.js +++ b/packages/insomnia-app/app/models/helpers/__tests__/git-repository-operations.test.ts @@ -1,23 +1,19 @@ -// @flow - import { globalBeforeEach } from '../../../__jest__/before-each'; import * as models from '../../index'; import { createGitRepository, deleteGitRepository } from '../git-repository-operations'; describe('gitRepositoryOperations', () => { beforeEach(globalBeforeEach); - describe('createGitRepository', () => { it('should create and link to workspace meta', async () => { - const repoId = `git_1`; + const repoId = 'git_1'; const workspaceId = 'wrk_1'; - - await createGitRepository(workspaceId, { _id: repoId }); - + await createGitRepository(workspaceId, { + _id: repoId, + }); const wMetas = await models.workspaceMeta.all(); expect(wMetas).toHaveLength(1); const wMeta = wMetas[0]; - expect(wMeta.parentId).toBe(workspaceId); expect(wMeta.gitRepositoryId).toBe(repoId); }); @@ -26,11 +22,8 @@ describe('gitRepositoryOperations', () => { describe('deleteGitRepository', () => { it('should delete', async () => { const repo = await models.gitRepository.create(); - expect(await models.gitRepository.all()).toHaveLength(1); - await deleteGitRepository(repo); - expect(await models.gitRepository.all()).toHaveLength(0); }); @@ -44,9 +37,7 @@ describe('gitRepositoryOperations', () => { cachedGitLastCommitTime: 123, cachedGitLastAuthor: 'abc', }); - await deleteGitRepository(repo); - expect(await models.gitRepository.all()).toHaveLength(0); const meta = await models.workspaceMeta.getByParentId(wrk._id); expect(meta).toStrictEqual( diff --git a/packages/insomnia-app/app/models/helpers/__tests__/is-model.test.js b/packages/insomnia-app/app/models/helpers/__tests__/is-model.test.ts similarity index 78% rename from packages/insomnia-app/app/models/helpers/__tests__/is-model.test.js rename to packages/insomnia-app/app/models/helpers/__tests__/is-model.test.ts index 073fac0f36..687e00a03f 100644 --- a/packages/insomnia-app/app/models/helpers/__tests__/is-model.test.js +++ b/packages/insomnia-app/app/models/helpers/__tests__/is-model.test.ts @@ -12,7 +12,6 @@ import { } from '../is-model'; import { generateId } from '../../../common/misc'; import { WorkspaceScopeKeys } from '../../workspace'; - const allTypes = models.types(); const allPrefixes = models.all().map(model => model.prefix); @@ -21,11 +20,19 @@ describe('isGrpcRequest', () => { const unsupported = difference(allTypes, supported); it.each(supported)('should return true: "%s"', type => { - expect(isGrpcRequest({ type })).toBe(true); + expect( + isGrpcRequest({ + type, + }), + ).toBe(true); }); it.each(unsupported)('should return false: "%s"', type => { - expect(isGrpcRequest({ type })).toBe(false); + expect( + isGrpcRequest({ + type, + }), + ).toBe(false); }); }); @@ -47,14 +54,21 @@ describe('isRequest', () => { const unsupported = difference(allTypes, supported); it.each(supported)('should return true: "%s"', type => { - expect(isRequest({ type })).toBe(true); + expect( + isRequest({ + type, + }), + ).toBe(true); }); it.each(unsupported)('should return false: "%s"', type => { - expect(isRequest({ type })).toBe(false); + expect( + isRequest({ + type, + }), + ).toBe(false); }); }); - // describe('isRequestId', () => { // const supported = [models.request.prefix]; // const unsupported = difference(allPrefixes, supported); @@ -73,11 +87,19 @@ describe('isRequestGroup', () => { const unsupported = difference(allTypes, supported); it.each(supported)('should return true: "%s"', type => { - expect(isRequestGroup({ type })).toBe(true); + expect( + isRequestGroup({ + type, + }), + ).toBe(true); }); it.each(unsupported)('should return false: "%s"', type => { - expect(isRequestGroup({ type })).toBe(false); + expect( + isRequestGroup({ + type, + }), + ).toBe(false); }); }); @@ -86,11 +108,19 @@ describe('isProtoFile', () => { const unsupported = difference(allTypes, supported); it.each(supported)('should return true: "%s"', type => { - expect(isProtoFile({ type })).toBe(true); + expect( + isProtoFile({ + type, + }), + ).toBe(true); }); it.each(unsupported)('should return false: "%s"', type => { - expect(isProtoFile({ type })).toBe(false); + expect( + isProtoFile({ + type, + }), + ).toBe(false); }); }); @@ -99,11 +129,19 @@ describe('isProtoDirectory', () => { const unsupported = difference(allTypes, supported); it.each(supported)('should return true: "%s"', type => { - expect(isProtoDirectory({ type })).toBe(true); + expect( + isProtoDirectory({ + type, + }), + ).toBe(true); }); it.each(unsupported)('should return false: "%s"', type => { - expect(isProtoDirectory({ type })).toBe(false); + expect( + isProtoDirectory({ + type, + }), + ).toBe(false); }); }); @@ -112,11 +150,19 @@ describe('isWorkspace', () => { const unsupported = difference(allTypes, supported); it.each(supported)('should return true: "%s"', type => { - expect(isWorkspace({ type })).toBe(true); + expect( + isWorkspace({ + type, + }), + ).toBe(true); }); it.each(unsupported)('should return false: "%s"', type => { - expect(isWorkspace({ type })).toBe(false); + expect( + isWorkspace({ + type, + }), + ).toBe(false); }); }); diff --git a/packages/insomnia-app/app/models/helpers/__tests__/query-all-workspace-urls.test.js b/packages/insomnia-app/app/models/helpers/__tests__/query-all-workspace-urls.test.ts similarity index 91% rename from packages/insomnia-app/app/models/helpers/__tests__/query-all-workspace-urls.test.js rename to packages/insomnia-app/app/models/helpers/__tests__/query-all-workspace-urls.test.ts index 6226be5e40..3f53f751ae 100644 --- a/packages/insomnia-app/app/models/helpers/__tests__/query-all-workspace-urls.test.js +++ b/packages/insomnia-app/app/models/helpers/__tests__/query-all-workspace-urls.test.ts @@ -1,5 +1,3 @@ -// @flow - import { globalBeforeEach } from '../../../__jest__/before-each'; import * as models from '../../index'; import { queryAllWorkspaceUrls } from '../query-all-workspace-urls'; @@ -8,32 +6,32 @@ describe('queryAllWorkspaceUrls', () => { beforeEach(globalBeforeEach); it('should return empty array when no requests exist', async () => { - const w = await models.workspace.create({ name: 'Workspace' }); + const w = await models.workspace.create({ + name: 'Workspace', + }); await expect(queryAllWorkspaceUrls(w, models.request.type)).resolves.toHaveLength(0); await expect(queryAllWorkspaceUrls(w, models.grpcRequest.type)).resolves.toHaveLength(0); }); it('should return urls and exclude that of the selected request', async () => { - const w = await models.workspace.create({ name: 'Workspace' }); - + const w = await models.workspace.create({ + name: 'Workspace', + }); const r1 = await models.request.create({ name: 'Request 1', parentId: w._id, url: 'r1.url', }); - const gr1 = await models.grpcRequest.create({ name: 'Grpc Request 1', parentId: w._id, url: 'gr1.url', }); - const gr2 = await models.grpcRequest.create({ name: 'Grpc Request 2', parentId: w._id, url: 'gr2.url', }); - const f2 = await models.requestGroup.create({ name: 'Folder 2', parentId: w._id, @@ -43,46 +41,40 @@ describe('queryAllWorkspaceUrls', () => { parentId: f2._id, url: 'r2.url', }); - // Should ignore all of the following await models.grpcRequest.create({ name: 'Duplicate grpc url', parentId: w._id, url: gr2.url, }); - await models.request.create({ name: 'Duplicate url', parentId: f2._id, url: r2.url, }); - await models.request.create({ name: 'Undefined url', parentId: f2._id, url: undefined, }); - await models.grpcRequest.create({ name: 'Undefined url', parentId: w._id, url: undefined, }); - - const w2 = await models.workspace.create({ name: 'Workspace 2' }); - + const w2 = await models.workspace.create({ + name: 'Workspace 2', + }); await models.request.create({ name: 'Different workspace', parentId: w2._id, url: 'diff.url', }); - await models.grpcRequest.create({ name: 'Different workspace', parentId: w2._id, url: 'diff.url', }); - // All items await expect(queryAllWorkspaceUrls(w, models.request.type)).resolves.toStrictEqual( expect.arrayContaining([r1.url, r2.url]), @@ -90,7 +82,6 @@ describe('queryAllWorkspaceUrls', () => { await expect(queryAllWorkspaceUrls(w, models.grpcRequest.type)).resolves.toStrictEqual( expect.arrayContaining([gr1.url, gr2.url]), ); - // Ignore url of the selected request id await expect(queryAllWorkspaceUrls(w, models.request.type, r1._id)).resolves.toStrictEqual( expect.arrayContaining([r2.url]), diff --git a/packages/insomnia-app/app/models/helpers/get-workspace-name.js b/packages/insomnia-app/app/models/helpers/get-workspace-name.ts similarity index 91% rename from packages/insomnia-app/app/models/helpers/get-workspace-name.js rename to packages/insomnia-app/app/models/helpers/get-workspace-name.ts index b2008a1b45..abb8e21bbc 100644 --- a/packages/insomnia-app/app/models/helpers/get-workspace-name.js +++ b/packages/insomnia-app/app/models/helpers/get-workspace-name.ts @@ -1,9 +1,7 @@ -// @flow - import type { ApiSpec } from '../api-spec'; import type { Workspace } from '../workspace'; import { isDesign } from './is-model'; -export default function getWorkspaceName(w: Workspace, s: ApiSpec): string { +export default function getWorkspaceName(w: Workspace, s: ApiSpec) { return isDesign(w) ? s.fileName : w.name; } diff --git a/packages/insomnia-app/app/models/helpers/git-repository-operations.js b/packages/insomnia-app/app/models/helpers/git-repository-operations.ts similarity index 86% rename from packages/insomnia-app/app/models/helpers/git-repository-operations.js rename to packages/insomnia-app/app/models/helpers/git-repository-operations.ts index 0a17ee1e79..429cce04de 100644 --- a/packages/insomnia-app/app/models/helpers/git-repository-operations.js +++ b/packages/insomnia-app/app/models/helpers/git-repository-operations.ts @@ -1,11 +1,12 @@ -// @flow import * as models from '../../models'; import type { GitRepository } from '../../models/git-repository'; -export const createGitRepository = async (workspaceId: string, repo: $Shape) => { +export const createGitRepository = async (workspaceId: string, repo: Partial) => { const newRepo = await models.gitRepository.create(repo); const meta = await models.workspaceMeta.getOrCreateByParentId(workspaceId); - await models.workspaceMeta.update(meta, { gitRepositoryId: newRepo._id }); + await models.workspaceMeta.update(meta, { + gitRepositoryId: newRepo._id, + }); }; export const deleteGitRepository = async (repo: GitRepository) => { diff --git a/packages/insomnia-app/app/models/helpers/is-model.js b/packages/insomnia-app/app/models/helpers/is-model.js deleted file mode 100644 index 58fcf28782..0000000000 --- a/packages/insomnia-app/app/models/helpers/is-model.js +++ /dev/null @@ -1,46 +0,0 @@ -// @flow -import type { BaseModel } from '../index'; -import { grpcRequest, request, requestGroup, protoFile, protoDirectory, workspace } from '../index'; -import type { Workspace } from '../workspace'; -import { WorkspaceScopeKeys } from '../../models/workspace'; - -export function isGrpcRequest(obj: BaseModel): boolean { - return obj.type === grpcRequest.type; -} - -export function isGrpcRequestId(id: string): boolean { - return id.startsWith(`${grpcRequest.prefix}_`); -} - -export function isRequest(obj: BaseModel): boolean { - return obj.type === request.type; -} - -// TODO: Invalid until we can ensure all requests are prefixed by the id correctly INS-341 -// export function isRequestId(id: string): boolean { -// return id.startsWith(`${request.prefix}_`); -// } - -export function isRequestGroup(obj: BaseModel): boolean { - return obj.type === requestGroup.type; -} - -export function isProtoFile(obj: BaseModel): boolean { - return obj.type === protoFile.type; -} - -export function isProtoDirectory(obj: BaseModel): boolean { - return obj.type === protoDirectory.type; -} - -export function isWorkspace(obj: BaseModel): boolean { - return obj.type === workspace.type; -} - -export function isDesign({ scope }: Workspace): boolean { - return scope === WorkspaceScopeKeys.design; -} - -export function isCollection({ scope }: Workspace): boolean { - return scope === WorkspaceScopeKeys.collection; -} diff --git a/packages/insomnia-app/app/models/helpers/is-model.ts b/packages/insomnia-app/app/models/helpers/is-model.ts new file mode 100644 index 0000000000..a2e032c270 --- /dev/null +++ b/packages/insomnia-app/app/models/helpers/is-model.ts @@ -0,0 +1,55 @@ +import type { BaseModel } from '../index'; +import { + grpcRequest, + request, + requestGroup, + protoFile, + protoDirectory, + workspace, +} from '../index'; +import type { Workspace } from '../workspace'; +import { WorkspaceScopeKeys } from '../../models/workspace'; +import { ProtoDirectory } from '../proto-directory'; +import { ProtoFile } from '../proto-file'; +import { RequestGroup } from '../request-group'; +import { Request } from '../request'; +import { GrpcRequest } from '../grpc-request'; + +export const isGrpcRequestId = (id: string | null) => ( + id?.startsWith(`${grpcRequest.prefix}_`) +); + +// TODO: Invalid until we can ensure all requests are prefixed by the id correctly INS-341 +// export const isRequestId = (id: string) => id.startsWith(`${request.prefix}_`); + +export const isGrpcRequest = (obj: Pick): obj is GrpcRequest => ( + obj.type === grpcRequest.type +); + +export const isRequest = (obj: Pick): obj is Request => ( + obj.type === request.type +); + +export const isRequestGroup = (obj: Pick): obj is RequestGroup => ( + obj.type === requestGroup.type +); + +export const isProtoFile = (obj: Pick): obj is ProtoFile => ( + obj.type === protoFile.type +); + +export const isProtoDirectory = (obj: Pick): obj is ProtoDirectory => ( + obj.type === protoDirectory.type +); + +export const isWorkspace = (obj: Pick): obj is Workspace => ( + obj.type === workspace.type +); + +export const isDesign = (obj: Partial) => ( + obj.scope === WorkspaceScopeKeys.design +); + +export const isCollection = (obj: Workspace) => ( + obj.scope === WorkspaceScopeKeys.collection +); diff --git a/packages/insomnia-app/app/models/helpers/query-all-workspace-urls.js b/packages/insomnia-app/app/models/helpers/query-all-workspace-urls.ts similarity index 53% rename from packages/insomnia-app/app/models/helpers/query-all-workspace-urls.js rename to packages/insomnia-app/app/models/helpers/query-all-workspace-urls.ts index 77c6b585fd..b05b119492 100644 --- a/packages/insomnia-app/app/models/helpers/query-all-workspace-urls.js +++ b/packages/insomnia-app/app/models/helpers/query-all-workspace-urls.ts @@ -1,17 +1,14 @@ -// @flow - -import * as db from '../../common/database'; -import * as models from '../index'; -import type { Workspace } from '../workspace'; -import type { GrpcRequest } from '../grpc-request'; +import { database as db } from '../../common/database'; +import { Request, type as RequestType } from '../request'; +import { GrpcRequest, type as GrpcRequestType } from '../grpc-request'; +import { Workspace } from '../workspace'; export const queryAllWorkspaceUrls = async ( - workspace: Workspace, - reqType: models.request.type | models.grpcRequest.type, - reqId?: string = 'n/a', + workspace: Workspace | null, + reqType: typeof RequestType | typeof GrpcRequestType, + reqId = 'n/a', ): Promise> => { const docs = await db.withDescendants(workspace, reqType); - const urls = docs .filter( (d: any) => @@ -19,7 +16,7 @@ export const queryAllWorkspaceUrls = async ( d._id !== reqId && // Not current request (d.url || ''), // Only ones with non-empty URLs ) + // @ts-expect-error -- TSCONVERSION .map((r: Request | GrpcRequest) => (r.url || '').trim()); - return Array.from(new Set(urls)); }; diff --git a/packages/insomnia-app/app/models/helpers/request-operations.js b/packages/insomnia-app/app/models/helpers/request-operations.ts similarity index 58% rename from packages/insomnia-app/app/models/helpers/request-operations.js rename to packages/insomnia-app/app/models/helpers/request-operations.ts index aae94e455b..cdcf914c64 100644 --- a/packages/insomnia-app/app/models/helpers/request-operations.js +++ b/packages/insomnia-app/app/models/helpers/request-operations.ts @@ -1,7 +1,7 @@ -// @flow +import { GrpcRequest } from '../grpc-request'; +import { Request } from '../request'; import * as models from '../index'; import { isGrpcRequest, isGrpcRequestId } from './is-model'; -import type { GrpcRequest } from '../grpc-request'; export function getById(requestId: string): Promise { return isGrpcRequestId(requestId) @@ -9,20 +9,24 @@ export function getById(requestId: string): Promise(request: T): Promise { +export function remove(request: Request | GrpcRequest) { return isGrpcRequest(request) ? models.grpcRequest.remove(request) : models.request.remove(request); } -export function update(request: T, patch: $Shape = {}): Promise { +export function update(request: T, patch: Partial = {}): Promise { + // @ts-expect-error -- TSCONVERSION return isGrpcRequest(request) ? models.grpcRequest.update(request, patch) + // @ts-expect-error -- TSCONVERSION : models.request.update(request, patch); } -export function duplicate(request: T, patch: $Shape = {}): Promise { +export function duplicate(request: T, patch: Partial = {}): Promise { + // @ts-expect-error -- TSCONVERSION return isGrpcRequest(request) ? models.grpcRequest.duplicate(request, patch) + // @ts-expect-error -- TSCONVERSION : models.request.duplicate(request, patch); } diff --git a/packages/insomnia-app/app/models/helpers/workspace-operations.js b/packages/insomnia-app/app/models/helpers/workspace-operations.ts similarity index 55% rename from packages/insomnia-app/app/models/helpers/workspace-operations.js rename to packages/insomnia-app/app/models/helpers/workspace-operations.ts index c53a176aab..adcfe93558 100644 --- a/packages/insomnia-app/app/models/helpers/workspace-operations.js +++ b/packages/insomnia-app/app/models/helpers/workspace-operations.ts @@ -1,23 +1,28 @@ -// @flow import * as models from '../index'; -import * as db from '../../common/database'; - +import { database as db } from '../../common/database'; import { isDesign } from './is-model'; import type { Workspace } from '../workspace'; import type { ApiSpec } from '../api-spec'; -export async function rename(w: Workspace, s: ApiSpec, name: string): Promise { +export async function rename(w: Workspace, s: ApiSpec, name: string) { if (isDesign(w)) { - await models.apiSpec.update(s, { fileName: name }); + await models.apiSpec.update(s, { + fileName: name, + }); } else { - await models.workspace.update(w, { name }); + await models.workspace.update(w, { + name, + }); } } -export async function duplicate(w: Workspace, name: string): Promise { - const newWorkspace = await db.duplicate(w, { name }); - await models.apiSpec.updateOrCreateForParentId(newWorkspace._id, { fileName: name }); +export async function duplicate(w: Workspace, name: string) { + const newWorkspace = await db.duplicate(w, { + name, + }); + await models.apiSpec.updateOrCreateForParentId(newWorkspace._id, { + fileName: name, + }); models.stats.incrementCreatedRequestsForDescendents(newWorkspace); - return newWorkspace; } diff --git a/packages/insomnia-app/app/models/index.js b/packages/insomnia-app/app/models/index.ts similarity index 88% rename from packages/insomnia-app/app/models/index.js rename to packages/insomnia-app/app/models/index.ts index d034c035f5..ca8fc7b653 100644 --- a/packages/insomnia-app/app/models/index.js +++ b/packages/insomnia-app/app/models/index.ts @@ -1,4 +1,3 @@ -// @flow import * as _apiSpec from './api-spec'; import * as _clientCertificate from './client-certificate'; import * as _cookieJar from './cookie-jar'; @@ -25,13 +24,15 @@ import * as _workspace from './workspace'; import * as _workspaceMeta from './workspace-meta'; import { generateId, pluralize } from '../common/misc'; -export type BaseModel = { - _id: string, - type: string, - parentId: string, - modified: number, - created: number, -}; +export interface BaseModel { + _id: string; + type: string; + parentId: string; + modified: number; + created: number; + isPrivate: boolean; + name: string; +} // Reference to each model export const apiSpec = _apiSpec; @@ -88,19 +89,20 @@ export function all() { protoDirectory, grpcRequest, grpcRequestMeta, - ]; + ] as const; } -export function types(): Array { +export function types() { return all().map(model => model.type); } -export function canSync(d: BaseModel): boolean { - if ((d: any).isPrivate) { +export function canSync(d: BaseModel) { + if (d.isPrivate) { return false; } const m = getModel(d.type); + if (!m) { return false; } @@ -108,11 +110,11 @@ export function canSync(d: BaseModel): boolean { return m.canSync || false; } -export function getModel(type: string): Object | null { +export function getModel(type: string) { return all().find(m => m.type === type) || null; } -export function mustGetModel(type: string): Object { +export function mustGetModel(type: string) { const model = getModel(type); if (!model) { @@ -127,8 +129,9 @@ export function canDuplicate(type: string) { return model ? model.canDuplicate : false; } -export function getModelName(type: string, count: number = 1) { +export function getModelName(type: string, count = 1) { const model = getModel(type); + if (!model) { return 'Unknown'; } else if (count === 1) { @@ -138,7 +141,7 @@ export function getModelName(type: string, count: number = 1) { } } -export async function initModel(type: string, ...sources: Array): Promise { +export async function initModel(type: string, ...sources: Array>): Promise { const model = getModel(type); if (!model) { @@ -160,7 +163,6 @@ export async function initModel(type: string, ...sources: Array(type: string, ...sources: Array(doc: T): T { - return doc; -} - -export function create(patch: $Shape = {}): Promise { - if (!patch.parentId) { - throw new Error(`New OAuth2Token missing \`parentId\` ${JSON.stringify(patch)}`); - } - - return db.docCreate(type, patch); -} - -export function update(token: OAuth2Token, patch: $Shape): Promise { - return db.docUpdate(token, patch); -} - -export function remove(token: OAuth2Token): Promise { - return db.remove(token); -} - -export function getByParentId(parentId: string): Promise { - return db.getWhere(type, { parentId }); -} - -export async function getOrCreateByParentId(parentId: string): Promise { - let token = await db.getWhere(type, { parentId }); - - if (!token) { - token = await create({ parentId }); - } - - return token; -} - -export function all(): Promise> { - return db.all(type); -} diff --git a/packages/insomnia-app/app/models/o-auth-2-token.ts b/packages/insomnia-app/app/models/o-auth-2-token.ts new file mode 100644 index 0000000000..042fcb318c --- /dev/null +++ b/packages/insomnia-app/app/models/o-auth-2-token.ts @@ -0,0 +1,88 @@ +import type { BaseModel } from './index'; +import { database as db } from '../common/database'; + +export type OAuth2Token = BaseModel & BaseOAuth2Token; + +export const name = 'OAuth 2.0 Token'; + +export const type = 'OAuth2Token'; + +export const prefix = 'oa2'; + +export const canDuplicate = false; + +export const canSync = false; + +interface BaseOAuth2Token { + refreshToken: string; + accessToken: string; + identityToken: string; + expiresAt: number | null; + // Should be Date.now() if valid + // Debug + xResponseId: string | null; + xError: string | null; + // Error handling + error: string; + errorDescription: string; + errorUri: string; +} + +export function init(): BaseOAuth2Token { + return { + refreshToken: '', + accessToken: '', + identityToken: '', + expiresAt: null, + // Should be Date.now() if valid + // Debug + xResponseId: null, + xError: null, + // Error handling + error: '', + errorDescription: '', + errorUri: '', + }; +} + +export function migrate(doc: OAuth2Token) { + return doc; +} + +export function create(patch: Partial = {}) { + if (!patch.parentId) { + throw new Error(`New OAuth2Token missing \`parentId\` ${JSON.stringify(patch)}`); + } + + return db.docCreate(type, patch); +} + +export function update(token: OAuth2Token, patch: Partial) { + return db.docUpdate(token, patch); +} + +export function remove(token: OAuth2Token) { + return db.remove(token); +} + +export function getByParentId(parentId: string) { + return db.getWhere(type, { parentId }); +} + +export async function getOrCreateByParentId(parentId: string) { + let token = await db.getWhere(type, { + parentId, + }); + + if (!token) { + token = await create({ + parentId, + }); + } + + return token; +} + +export function all() { + return db.all(type); +} diff --git a/packages/insomnia-app/app/models/plugin-data.js b/packages/insomnia-app/app/models/plugin-data.js deleted file mode 100644 index 4df9cfeda1..0000000000 --- a/packages/insomnia-app/app/models/plugin-data.js +++ /dev/null @@ -1,58 +0,0 @@ -// @flow -import * as db from '../common/database'; -import type { BaseModel } from './index'; - -export const name = 'PluginData'; -export const type = 'PluginData'; -export const prefix = 'plg'; -export const canDuplicate = false; -export const canSync = false; - -type BasePluginData = { - plugin: string, - key: string, - value: string, -}; - -export type PluginData = BaseModel & BasePluginData; - -export function init(): BasePluginData { - return { - plugin: '', - key: '', - value: '', - }; -} - -export function migrate(doc: PluginData): PluginData { - return doc; -} - -export function create(patch: $Shape = {}): Promise { - return db.docCreate(type, patch); -} - -export async function update(doc: PluginData, patch: $Shape): Promise { - return db.docUpdate(doc, patch); -} - -export async function upsertByKey(plugin: string, key: string, value: string): Promise { - const doc = await getByKey(plugin, key); - return doc ? update(doc, { value }) : create({ plugin, key, value }); -} - -export async function removeByKey(plugin: string, key: string): Promise { - return db.removeWhere(type, { plugin, key }); -} - -export async function all(plugin: string): Promise> { - return db.find(type, { plugin }); -} - -export async function removeAll(plugin: string): Promise { - return db.removeWhere(type, { plugin }); -} - -export async function getByKey(plugin: string, key: string): Promise { - return db.getWhere(type, { plugin, key }); -} diff --git a/packages/insomnia-app/app/models/plugin-data.ts b/packages/insomnia-app/app/models/plugin-data.ts new file mode 100644 index 0000000000..ce83f5a56a --- /dev/null +++ b/packages/insomnia-app/app/models/plugin-data.ts @@ -0,0 +1,69 @@ +import { database as db } from '../common/database'; +import type { BaseModel } from './index'; + +export const name = 'PluginData'; + +export const type = 'PluginData'; + +export const prefix = 'plg'; + +export const canDuplicate = false; + +export const canSync = false; + +interface BasePluginData { + plugin: string; + key: string; + value: string; +} + +export type PluginData = BaseModel & BasePluginData; + +export function init(): BasePluginData { + return { + plugin: '', + key: '', + value: '', + }; +} + +export function migrate(doc: PluginData) { + return doc; +} + +export function create(patch: Partial = {}) { + return db.docCreate(type, patch); +} + +export async function update(doc: PluginData, patch: Partial) { + return db.docUpdate(doc, patch); +} + +export async function upsertByKey(plugin: string, key: string, value: string) { + const doc = await getByKey(plugin, key); + return doc + ? update(doc, { + value, + }) + : create({ + plugin, + key, + value, + }); +} + +export async function removeByKey(plugin: string, key: string) { + return db.removeWhere(type, { plugin, key }); +} + +export async function all(plugin: string) { + return db.find(type, { plugin }); +} + +export async function removeAll(plugin: string) { + return db.removeWhere(type, { plugin }); +} + +export async function getByKey(plugin: string, key: string) { + return db.getWhere(type, { plugin, key }); +} diff --git a/packages/insomnia-app/app/models/proto-directory.js b/packages/insomnia-app/app/models/proto-directory.js deleted file mode 100644 index 48ebd32891..0000000000 --- a/packages/insomnia-app/app/models/proto-directory.js +++ /dev/null @@ -1,59 +0,0 @@ -// @flow -import * as db from '../common/database'; -import type { BaseModel } from './index'; -import { generateId } from '../common/misc'; - -export const name = 'Proto Directory'; -export const type = 'ProtoDirectory'; -export const prefix = 'pd'; -export const canDuplicate = true; -export const canSync = true; - -type BaseProtoDirectory = { - name: string, -}; - -export type ProtoDirectory = BaseModel & BaseProtoDirectory; - -export function init(): BaseProtoDirectory { - return { - name: 'New Proto Directory', - }; -} - -export function migrate(doc: ProtoDirectory): ProtoDirectory { - return doc; -} - -export function createId(): string { - return generateId(prefix); -} - -export function create(patch: $Shape = {}): Promise { - if (!patch.parentId) { - throw new Error('New ProtoDirectory missing `parentId`'); - } - - return db.docCreate(type, patch); -} - -export function getById(_id: string): Promise { - return db.getWhere(type, { _id }); -} - -export function getByParentId(parentId: string): Promise { - return db.getWhere(type, { parentId }); -} - -export function remove(obj: ProtoDirectory): Promise { - return db.remove(obj); -} - -export async function batchRemoveIds(ids: Array): Promise { - const dirs = await db.find(type, { _id: { $in: ids } }); - await db.batchModifyDocs({ upsert: [], remove: dirs }); -} - -export function all(): Promise> { - return db.all(type); -} diff --git a/packages/insomnia-app/app/models/proto-directory.ts b/packages/insomnia-app/app/models/proto-directory.ts new file mode 100644 index 0000000000..be7881a2aa --- /dev/null +++ b/packages/insomnia-app/app/models/proto-directory.ts @@ -0,0 +1,69 @@ +import { database as db } from '../common/database'; +import type { BaseModel } from './index'; +import { generateId } from '../common/misc'; + +export const name = 'Proto Directory'; + +export const type = 'ProtoDirectory'; + +export const prefix = 'pd'; + +export const canDuplicate = true; + +export const canSync = true; + +interface BaseProtoDirectory { + name: string; +} + +export type ProtoDirectory = BaseModel & BaseProtoDirectory; + +export function init(): BaseProtoDirectory { + return { + name: 'New Proto Directory', + }; +} + +export function migrate(doc: ProtoDirectory) { + return doc; +} + +export function createId() { + return generateId(prefix); +} + +export function create(patch: Partial = {}) { + if (!patch.parentId) { + throw new Error('New ProtoDirectory missing `parentId`'); + } + + return db.docCreate(type, patch); +} + +export function getById(_id: string) { + return db.getWhere(type, { _id }); +} + +export function getByParentId(parentId: string) { + return db.getWhere(type, { parentId }); +} + +export function remove(obj: ProtoDirectory) { + return db.remove(obj); +} + +export async function batchRemoveIds(ids: Array) { + const dirs = await db.find(type, { + _id: { + $in: ids, + }, + }); + await db.batchModifyDocs({ + upsert: [], + remove: dirs, + }); +} + +export function all() { + return db.all(type); +} diff --git a/packages/insomnia-app/app/models/proto-file.js b/packages/insomnia-app/app/models/proto-file.js deleted file mode 100644 index 14934a458c..0000000000 --- a/packages/insomnia-app/app/models/proto-file.js +++ /dev/null @@ -1,64 +0,0 @@ -// @flow -import * as db from '../common/database'; -import type { BaseModel } from './index'; - -export const name = 'Proto File'; -export const type = 'ProtoFile'; -export const prefix = 'pf'; -export const canDuplicate = true; -export const canSync = true; - -type BaseProtoFile = { - name: string, - protoText: string, -}; - -export type ProtoFile = BaseModel & BaseProtoFile; - -export function init(): BaseProtoFile { - return { - name: 'New Proto File', - protoText: '', - }; -} - -export function migrate(doc: ProtoFile): ProtoFile { - return doc; -} - -export function create(patch: $Shape = {}): Promise { - if (!patch.parentId) { - throw new Error('New ProtoFile missing `parentId`'); - } - - return db.docCreate(type, patch); -} - -export function remove(protoFile: ProtoFile): Promise { - return db.remove(protoFile); -} - -export async function batchRemoveIds(ids: Array): Promise { - const files = await db.find(type, { _id: { $in: ids } }); - await db.batchModifyDocs({ upsert: [], remove: files }); -} - -export function update(protoFile: ProtoFile, patch: $Shape = {}): Promise { - return db.docUpdate(protoFile, patch); -} - -export function getById(_id: string): Promise { - return db.getWhere(type, { _id }); -} - -export function getByParentId(parentId: string): Promise { - return db.getWhere(type, { parentId }); -} - -export function findByParentId(parentId: string): Promise> { - return db.find(type, { parentId }); -} - -export function all(): Promise> { - return db.all(type); -} diff --git a/packages/insomnia-app/app/models/proto-file.ts b/packages/insomnia-app/app/models/proto-file.ts new file mode 100644 index 0000000000..2f2d5f1b1a --- /dev/null +++ b/packages/insomnia-app/app/models/proto-file.ts @@ -0,0 +1,74 @@ +import { database as db } from '../common/database'; +import type { BaseModel } from './index'; + +export const name = 'Proto File'; + +export const type = 'ProtoFile'; + +export const prefix = 'pf'; + +export const canDuplicate = true; + +export const canSync = true; + +interface BaseProtoFile { + name: string; + protoText: string; +} + +export type ProtoFile = BaseModel & BaseProtoFile; + +export function init(): BaseProtoFile { + return { + name: 'New Proto File', + protoText: '', + }; +} + +export function migrate(doc: ProtoFile) { + return doc; +} + +export function create(patch: Partial = {}) { + if (!patch.parentId) { + throw new Error('New ProtoFile missing `parentId`'); + } + + return db.docCreate(type, patch); +} + +export function remove(protoFile: ProtoFile) { + return db.remove(protoFile); +} + +export async function batchRemoveIds(ids: Array) { + const files = await db.find(type, { + _id: { + $in: ids, + }, + }); + await db.batchModifyDocs({ + upsert: [], + remove: files, + }); +} + +export function update(protoFile: ProtoFile, patch: Partial = {}) { + return db.docUpdate(protoFile, patch); +} + +export function getById(_id: string) { + return db.getWhere(type, { _id }); +} + +export function getByParentId(parentId: string) { + return db.getWhere(type, { parentId }); +} + +export function findByParentId(parentId: string) { + return db.find(type, { parentId }); +} + +export function all() { + return db.all(type); +} diff --git a/packages/insomnia-app/app/models/request-group-meta.js b/packages/insomnia-app/app/models/request-group-meta.js deleted file mode 100644 index b6bbd643a8..0000000000 --- a/packages/insomnia-app/app/models/request-group-meta.js +++ /dev/null @@ -1,50 +0,0 @@ -// @flow - -import * as db from '../common/database'; -import type { BaseModel } from './index'; - -export const name = 'Folder Meta'; -export const type = 'RequestGroupMeta'; -export const prefix = 'fldm'; -export const canDuplicate = false; -export const canSync = false; - -type BaseRequestGroupMeta = { - collapsed: boolean, -}; - -export type RequestGroupMeta = BaseModel & BaseRequestGroupMeta; - -export function init() { - return { - parentId: null, - collapsed: false, - }; -} - -export function migrate(doc: RequestGroupMeta): RequestGroupMeta { - return doc; -} - -export function create(patch: $Shape = {}): Promise { - if (!patch.parentId) { - throw new Error('New RequestGroupMeta missing `parentId`: ' + JSON.stringify(patch)); - } - - return db.docCreate(type, patch); -} - -export function update( - requestGroupMeta: RequestGroupMeta, - patch: $Shape, -): Promise { - return db.docUpdate(requestGroupMeta, patch); -} - -export function getByParentId(parentId: string): Promise { - return db.getWhere(type, { parentId }); -} - -export function all(): Promise> { - return db.all(type); -} diff --git a/packages/insomnia-app/app/models/request-group-meta.ts b/packages/insomnia-app/app/models/request-group-meta.ts new file mode 100644 index 0000000000..ba8c234444 --- /dev/null +++ b/packages/insomnia-app/app/models/request-group-meta.ts @@ -0,0 +1,49 @@ +import { database as db } from '../common/database'; +import type { BaseModel } from './index'; + +export const name = 'Folder Meta'; + +export const type = 'RequestGroupMeta'; + +export const prefix = 'fldm'; + +export const canDuplicate = false; + +export const canSync = false; + +interface BaseRequestGroupMeta { + collapsed: boolean; +} + +export type RequestGroupMeta = BaseModel & BaseRequestGroupMeta; + +export function init() { + return { + parentId: null, + collapsed: false, + }; +} + +export function migrate(doc: RequestGroupMeta) { + return doc; +} + +export function create(patch: Partial = {}) { + if (!patch.parentId) { + throw new Error('New RequestGroupMeta missing `parentId`: ' + JSON.stringify(patch)); + } + + return db.docCreate(type, patch); +} + +export function update(requestGroupMeta: RequestGroupMeta, patch: Partial) { + return db.docUpdate(requestGroupMeta, patch); +} + +export function getByParentId(parentId: string) { + return db.getWhere(type, { parentId }); +} + +export function all() { + return db.all(type); +} diff --git a/packages/insomnia-app/app/models/request-group.js b/packages/insomnia-app/app/models/request-group.js deleted file mode 100644 index a1788f866e..0000000000 --- a/packages/insomnia-app/app/models/request-group.js +++ /dev/null @@ -1,86 +0,0 @@ -// @flow -import * as db from '../common/database'; -import type { BaseModel } from './index'; - -export const name = 'Folder'; -export const type = 'RequestGroup'; -export const prefix = 'fld'; -export const canDuplicate = true; -export const canSync = true; - -type BaseRequestGroup = { - name: string, - description: string, - environment: Object, - environmentPropertyOrder: Object | null, - metaSortKey: number, -}; - -export type RequestGroup = BaseModel & BaseRequestGroup; - -export function init(): BaseRequestGroup { - return { - name: 'New Folder', - description: '', - environment: {}, - environmentPropertyOrder: null, - metaSortKey: -1 * Date.now(), - }; -} - -export function migrate(doc: RequestGroup) { - return doc; -} - -export function create(patch: $Shape = {}): Promise { - if (!patch.parentId) { - throw new Error('New RequestGroup missing `parentId`: ' + JSON.stringify(patch)); - } - - return db.docCreate(type, patch); -} - -export function update( - requestGroup: RequestGroup, - patch: $Shape = {}, -): Promise { - return db.docUpdate(requestGroup, patch); -} - -export function getById(id: string): Promise { - return db.get(type, id); -} - -export function findByParentId(parentId: string): Promise> { - return db.find(type, { parentId }); -} - -export function remove(requestGroup: RequestGroup): Promise { - return db.remove(requestGroup); -} - -export function all(): Promise> { - return db.all(type); -} - -export async function duplicate( - requestGroup: RequestGroup, - patch: $Shape = {}, -): Promise { - if (!patch.name) { - patch.name = `${requestGroup.name} (Copy)`; - } - - // Get sort key of next request - const q = { metaSortKey: { $gt: requestGroup.metaSortKey } }; - const [nextRequestGroup] = await db.find(type, q, { metaSortKey: 1 }); - const nextSortKey = nextRequestGroup - ? nextRequestGroup.metaSortKey - : requestGroup.metaSortKey + 100; - - // Calculate new sort key - const sortKeyIncrement = (nextSortKey - requestGroup.metaSortKey) / 2; - const metaSortKey = requestGroup.metaSortKey + sortKeyIncrement; - - return db.duplicate(requestGroup, { metaSortKey, ...patch }); -} diff --git a/packages/insomnia-app/app/models/request-group.ts b/packages/insomnia-app/app/models/request-group.ts new file mode 100644 index 0000000000..f1240d59d4 --- /dev/null +++ b/packages/insomnia-app/app/models/request-group.ts @@ -0,0 +1,93 @@ +import { database as db } from '../common/database'; +import type { BaseModel } from './index'; + +export const name = 'Folder'; + +export const type = 'RequestGroup'; + +export const prefix = 'fld'; + +export const canDuplicate = true; + +export const canSync = true; +interface BaseRequestGroup { + name: string; + description: string; + environment: Record; + environmentPropertyOrder: Record | null; + metaSortKey: number; +} + +export type RequestGroup = BaseModel & BaseRequestGroup; + +export function init(): BaseRequestGroup { + return { + name: 'New Folder', + description: '', + environment: {}, + environmentPropertyOrder: null, + metaSortKey: -1 * Date.now(), + }; +} + +export function migrate(doc: RequestGroup) { + return doc; +} + +export function create(patch: Partial = {}) { + if (!patch.parentId) { + throw new Error('New RequestGroup missing `parentId`: ' + JSON.stringify(patch)); + } + + return db.docCreate(type, patch); +} + +export function update(requestGroup: RequestGroup, patch: Partial = {}) { + return db.docUpdate(requestGroup, patch); +} + +export function getById(id: string) { + return db.get(type, id); +} + +export function findByParentId(parentId: string) { + return db.find(type, { parentId }); +} + +export function remove(requestGroup: RequestGroup) { + return db.remove(requestGroup); +} + +export function all() { + return db.all(type); +} + +export async function duplicate(requestGroup: RequestGroup, patch: Partial = {}) { + if (!patch.name) { + patch.name = `${requestGroup.name} (Copy)`; + } + + // Get sort key of next request + const q = { + metaSortKey: { + $gt: requestGroup.metaSortKey, + }, + }; + + // @ts-expect-error -- TSCONVERSION appears to be a genuine error + const [nextRequestGroup] = await db.find(type, q, { + metaSortKey: 1, + }); + + const nextSortKey = nextRequestGroup + ? nextRequestGroup.metaSortKey + : requestGroup.metaSortKey + 100; + + // Calculate new sort key + const sortKeyIncrement = (nextSortKey - requestGroup.metaSortKey) / 2; + const metaSortKey = requestGroup.metaSortKey + sortKeyIncrement; + return db.duplicate(requestGroup, { + metaSortKey, + ...patch, + }); +} diff --git a/packages/insomnia-app/app/models/request-meta.js b/packages/insomnia-app/app/models/request-meta.ts similarity index 62% rename from packages/insomnia-app/app/models/request-meta.js rename to packages/insomnia-app/app/models/request-meta.ts index f9fdc82590..8f1e3a41dd 100644 --- a/packages/insomnia-app/app/models/request-meta.js +++ b/packages/insomnia-app/app/models/request-meta.ts @@ -1,25 +1,28 @@ -// @flow -import * as db from '../common/database'; +import { database as db } from '../common/database'; import { PREVIEW_MODE_FRIENDLY } from '../common/constants'; import type { BaseModel } from './index'; export const name = 'Request Meta'; + export const type = 'RequestMeta'; + export const prefix = 'reqm'; + export const canDuplicate = false; + export const canSync = false; -type BaseRequestMeta = { - parentId: string, - previewMode: string, - responseFilter: string, - responseFilterHistory: Array, - activeResponseId: string | null, - savedRequestBody: Object, - pinned: boolean, - lastActive: number, - downloadPath: string | null, -}; +interface BaseRequestMeta { + parentId: string; + previewMode: string; + responseFilter: string; + responseFilterHistory: Array; + activeResponseId: string | null; + savedRequestBody: Record; + pinned: boolean; + lastActive: number; + downloadPath: string | null; +} export type RequestMeta = BaseModel & BaseRequestMeta; @@ -37,28 +40,27 @@ export function init() { }; } -export function migrate(doc: RequestMeta): RequestMeta { +export function migrate(doc: RequestMeta) { return doc; } -export function create(patch: $Shape = {}) { +export function create(patch: Partial = {}) { if (!patch.parentId) { throw new Error('New RequestMeta missing `parentId` ' + JSON.stringify(patch)); } // expectParentToBeRequest(patch.parentId); - - return db.docCreate(type, patch); + return db.docCreate(type, patch); } -export function update(requestMeta: RequestMeta, patch: $Shape) { +export function update(requestMeta: RequestMeta, patch: Partial) { // expectParentToBeRequest(patch.parentId || requestMeta.parentId); - return db.docUpdate(requestMeta, patch); + return db.docUpdate(requestMeta, patch); } -export function getByParentId(parentId: string) { +export function getByParentId(parentId: string): Promise { // expectParentToBeRequest(parentId); - return db.getWhere(type, { parentId }); + return db.getWhere(type, { parentId }); } export async function getOrCreateByParentId(parentId: string) { @@ -71,19 +73,24 @@ export async function getOrCreateByParentId(parentId: string) { return create({ parentId }); } -export async function updateOrCreateByParentId(parentId: string, patch: $Shape) { +export async function updateOrCreateByParentId(parentId: string, patch: Partial) { const requestMeta = await getByParentId(parentId); if (requestMeta) { return update(requestMeta, patch); } else { - const newPatch = Object.assign({ parentId }, patch); + const newPatch = Object.assign( + { + parentId, + }, + patch, + ); return create(newPatch); } } -export function all(): Promise> { - return db.all(type); +export function all() { + return db.all(type); } // TODO: Ensure the parent of RequestMeta can only be a Request - INS-341 diff --git a/packages/insomnia-app/app/models/request-version.js b/packages/insomnia-app/app/models/request-version.ts similarity index 70% rename from packages/insomnia-app/app/models/request-version.js rename to packages/insomnia-app/app/models/request-version.ts index a213efa8cb..9856edca87 100644 --- a/packages/insomnia-app/app/models/request-version.js +++ b/packages/insomnia-app/app/models/request-version.ts @@ -1,20 +1,23 @@ -// @flow import deepEqual from 'deep-equal'; import * as models from './index'; -import * as db from '../common/database'; +import { database as db } from '../common/database'; import { compressObject, decompressObject } from '../common/misc'; import type { BaseModel } from './index'; import type { Request } from './request'; export const name = 'Request Version'; + export const type = 'RequestVersion'; + export const prefix = 'rvr'; + export const canDuplicate = false; + export const canSync = false; -type BaseRequestVersion = { - compressedRequest: string | null, -}; +interface BaseRequestVersion { + compressedRequest: string | null; +} export type RequestVersion = BaseModel & BaseRequestVersion; @@ -35,42 +38,46 @@ export function init() { }; } -export function migrate(doc: RequestVersion): RequestVersion { +export function migrate(doc: RequestVersion) { return doc; } -export function getById(id: string): Promise { - return db.get(type, id); +export function getById(id: string) { + return db.get(type, id); } -export async function create(request: Request): Promise { +export async function create(request: Request) { + // @ts-expect-error -- TSCONVERSION if (!request.type === models.request.type) { throw new Error(`New ${type} was not given a valid ${models.request.type} instance`); } const parentId = request._id; const latestRequestVersion: RequestVersion | null = await getLatestByParentId(parentId); - const latestRequest = latestRequestVersion ? decompressObject(latestRequestVersion.compressedRequest) : null; const hasChanged = _diffRequests(latestRequest, request); + if (hasChanged || !latestRequestVersion) { // Create a new version if the request has been modified const compressedRequest = compressObject(request); - return db.docCreate(type, { parentId, compressedRequest }); + return db.docCreate(type, { + parentId, + compressedRequest, + }); } else { // Re-use the latest version if not modified since return latestRequestVersion; } } -export function getLatestByParentId(parentId: string): Promise { - return db.getMostRecentlyModified(type, { parentId }); +export function getLatestByParentId(parentId: string) { + return db.getMostRecentlyModified(type, { parentId }); } -export async function restore(requestVersionId: string): Promise { +export async function restore(requestVersionId: string) { const requestVersion = await getById(requestVersionId); // Older responses won't have versions saved with them @@ -79,7 +86,7 @@ export async function restore(requestVersionId: string): Promise } const requestPatch = decompressObject(requestVersion.compressedRequest); - const originalRequest: Request | null = await models.request.getById(requestPatch._id); + const originalRequest = await models.request.getById(requestPatch._id); if (!originalRequest) { return null; @@ -93,7 +100,7 @@ export async function restore(requestVersionId: string): Promise return models.request.update(originalRequest, requestPatch); } -function _diffRequests(rOld: Request | null, rNew: Request): boolean { +function _diffRequests(rOld: Request | null, rNew: Request) { if (!rOld) { return true; } @@ -112,6 +119,6 @@ function _diffRequests(rOld: Request | null, rNew: Request): boolean { return false; } -export function all(): Promise> { - return db.all(type); +export function all() { + return db.all(type); } diff --git a/packages/insomnia-app/app/models/request.js b/packages/insomnia-app/app/models/request.ts similarity index 76% rename from packages/insomnia-app/app/models/request.js rename to packages/insomnia-app/app/models/request.ts index 64b3213d6a..aac2b2eeab 100644 --- a/packages/insomnia-app/app/models/request.js +++ b/packages/insomnia-app/app/models/request.ts @@ -1,4 +1,3 @@ -// @flow import type { BaseModel } from './index'; import { AUTH_ASAP, @@ -22,73 +21,76 @@ import { METHOD_GET, METHOD_POST, } from '../common/constants'; -import * as db from '../common/database'; +import { database as db } from '../common/database'; import { getContentTypeHeader } from '../common/misc'; import { deconstructQueryStringToParams } from 'insomnia-url'; import { GRANT_TYPE_AUTHORIZATION_CODE } from '../network/o-auth-2/constants'; import { SIGNATURE_METHOD_HMAC_SHA1 } from '../network/o-auth-1/constants'; export const name = 'Request'; + export const type = 'Request'; + export const prefix = 'req'; + export const canDuplicate = true; + export const canSync = true; -export type RequestAuthentication = Object; +export type RequestAuthentication = Record; -export type RequestHeader = { - name: string, - value: string, - description?: string, - disabled?: boolean, -}; +export interface RequestHeader { + name: string; + value: string; + description?: string; + disabled?: boolean; +} -export type RequestParameter = { - name: string, - value: string, - disabled?: boolean, - id?: string, - fileName?: string, -}; +export interface RequestParameter { + name: string; + value: string; + disabled?: boolean; + id?: string; + fileName?: string; +} -export type RequestBodyParameter = { - name: string, - value: string, - description?: string, - disabled?: boolean, - multiline?: string, - id?: string, - fileName?: string, - type?: string, -}; +export interface RequestBodyParameter { + name: string; + value: string; + description?: string; + disabled?: boolean; + multiline?: string; + id?: string; + fileName?: string; + type?: string; +} -export type RequestBody = { - mimeType?: string | null, - text?: string, - fileName?: string, - params?: Array, -}; - -type BaseRequest = { - url: string, - name: string, - description: string, - method: string, - body: RequestBody, - parameters: Array, - headers: Array, - authentication: RequestAuthentication, - metaSortKey: number, - isPrivate: boolean, +export interface RequestBody { + mimeType?: string | null; + text?: string; + fileName?: string; + params?: Array; +} +interface BaseRequest { + url: string; + name: string; + description: string; + method: string; + body: RequestBody; + parameters: Array; + headers: Array; + authentication: RequestAuthentication; + metaSortKey: number; + isPrivate: boolean; // Settings - settingStoreCookies: boolean, - settingSendCookies: boolean, - settingDisableRenderRequestBody: boolean, - settingEncodeUrl: boolean, - settingRebuildPath: boolean, - settingFollowRedirects: string, -}; + settingStoreCookies: boolean; + settingSendCookies: boolean; + settingDisableRenderRequestBody: boolean; + settingEncodeUrl: boolean; + settingRebuildPath: boolean; + settingFollowRedirects: string; +} export type Request = BaseModel & BaseRequest; @@ -104,7 +106,6 @@ export function init(): BaseRequest { authentication: {}, metaSortKey: -1 * Date.now(), isPrivate: false, - // Settings settingStoreCookies: true, settingSendCookies: true, @@ -195,7 +196,9 @@ export function newAuth(type: string, oldAuth: RequestAuthentication = {}): Requ // Types needing no defaults case AUTH_NETRC: default: - return { type }; + return { + type, + }; } } @@ -205,21 +208,32 @@ export function newBodyNone(): RequestBody { export function newBodyRaw(rawBody: string, contentType?: string): RequestBody { if (typeof contentType !== 'string') { - return { text: rawBody }; + return { + text: rawBody, + }; } const mimeType = contentType.split(';')[0]; - return { mimeType, text: rawBody }; + return { + mimeType, + text: rawBody, + }; } export function newBodyGraphQL(rawBody: string): RequestBody { try { // Only strip the newlines if rawBody is a parsable JSON JSON.parse(rawBody); - return { mimeType: CONTENT_TYPE_GRAPHQL, text: rawBody.replace(/\\\\n/g, '') }; + return { + mimeType: CONTENT_TYPE_GRAPHQL, + text: rawBody.replace(/\\\\n/g, ''), + }; } catch (e) { if (e instanceof SyntaxError) { - return { mimeType: CONTENT_TYPE_GRAPHQL, text: rawBody }; + return { + mimeType: CONTENT_TYPE_GRAPHQL, + text: rawBody, + }; } else { throw e; } @@ -254,35 +268,34 @@ export function migrate(doc: Request): Request { return doc; } -export function create(patch: $Shape = {}): Promise { +export function create(patch: Partial = {}) { if (!patch.parentId) { throw new Error(`New Requests missing \`parentId\`: ${JSON.stringify(patch)}`); } - return db.docCreate(type, patch); + return db.docCreate(type, patch); } export function getById(id: string): Promise { return db.get(type, id); } -export function findByParentId(parentId: string): Promise> { - return db.find(type, { parentId: parentId }); +export function findByParentId(parentId: string) { + return db.find(type, { parentId: parentId }); } -export function update(request: Request, patch: $Shape): Promise { - return db.docUpdate(request, patch); +export function update(request: Request, patch: Partial) { + return db.docUpdate(request, patch); } export function updateMimeType( request: Request, mimeType: string, - doCreate: boolean = false, + doCreate = false, savedBody: RequestBody = {}, -): Promise { +) { let headers = request.headers ? [...request.headers] : []; const contentTypeHeader = getContentTypeHeader(headers); - // GraphQL uses JSON content-type const contentTypeHeaderValue = mimeType === CONTENT_TYPE_GRAPHQL ? CONTENT_TYPE_JSON : mimeType; @@ -293,8 +306,10 @@ export function updateMimeType( // Check if we are converting to/from variants of XML or JSON let leaveContentTypeAlone = false; + if (contentTypeHeader && mimeType) { const current = contentTypeHeader.value; + if (current.includes('xml') && mimeType.includes('xml')) { leaveContentTypeAlone = true; } else if (current.includes('json') && mimeType.includes('json')) { @@ -305,8 +320,8 @@ export function updateMimeType( // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // 1. Update Content-Type header // // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // - const hasBody = typeof mimeType === 'string'; + if (!hasBody) { headers = headers.filter(h => h !== contentTypeHeader); } else if (mimeType === CONTENT_TYPE_OTHER) { @@ -314,26 +329,29 @@ export function updateMimeType( } else if (mimeType && contentTypeHeader && !leaveContentTypeAlone) { contentTypeHeader.value = contentTypeHeaderValue; } else if (mimeType && !contentTypeHeader) { - headers.push({ name: 'Content-Type', value: contentTypeHeaderValue }); + headers.push({ + name: 'Content-Type', + value: contentTypeHeaderValue, + }); } // ~~~~~~~~~~~~~~~~~~~~~~~~~~ // // 2. Make a new request body // // ~~~~~~~~~~~~~~~~~~~~~~~~~~ // - let body; - const oldBody = Object.keys(savedBody).length === 0 ? request.body : savedBody; if (mimeType === CONTENT_TYPE_FORM_URLENCODED) { // Urlencoded body = oldBody.params ? newBodyFormUrlEncoded(oldBody.params) + // @ts-expect-error -- TSCONVERSION : newBodyFormUrlEncoded(deconstructQueryStringToParams(oldBody.text)); - } else if (mimeType === CONTENT_TYPE_FORM_DATA) { - // Form Data - body = oldBody.params + } else if (mimeType === CONTENT_TYPE_FORM_DATA) { + // Form Data + body = oldBody.params ? newBodyForm(oldBody.params) + // @ts-expect-error -- TSCONVERSION : newBodyForm(deconstructQueryStringToParams(oldBody.text)); } else if (mimeType === CONTENT_TYPE_FILE) { // File @@ -342,6 +360,7 @@ export function updateMimeType( if (contentTypeHeader) { contentTypeHeader.value = CONTENT_TYPE_JSON; } + body = newBodyGraphQL(oldBody.text || ''); } else if (typeof mimeType !== 'string') { // No body @@ -354,16 +373,21 @@ export function updateMimeType( // ~~~~~~~~~~~~~~~~~~~~~~~~ // // 2. create/update request // // ~~~~~~~~~~~~~~~~~~~~~~~~ // - if (doCreate) { - const newRequest: Request = Object.assign({}, request, { headers, body }); + const newRequest: Request = Object.assign({}, request, { + headers, + body, + }); return create(newRequest); } else { - return update(request, { headers, body }); + return update(request, { + headers, + body, + }); } } -export async function duplicate(request: Request, patch: $Shape = {}): Promise { +export async function duplicate(request: Request, patch: Partial = {}) { // Only set name and "(Copy)" if the patch does // not define it and the request itself has a name. // Otherwise leave it blank so the request URL can @@ -373,23 +397,34 @@ export async function duplicate(request: Request, patch: $Shape = {}): } // Get sort key of next request - const q = { metaSortKey: { $gt: request.metaSortKey } }; - const [nextRequest] = await db.find(type, q, { metaSortKey: 1 }); - const nextSortKey = nextRequest ? nextRequest.metaSortKey : request.metaSortKey + 100; + const q = { + metaSortKey: { + $gt: request.metaSortKey, + }, + }; + // @ts-expect-error -- TSCONVERSION appears to be a genuine error + const [nextRequest] = await db.find(type, q, { + metaSortKey: 1, + }); + + const nextSortKey = nextRequest ? nextRequest.metaSortKey : request.metaSortKey + 100; // Calculate new sort key const sortKeyIncrement = (nextSortKey - request.metaSortKey) / 2; const metaSortKey = request.metaSortKey + sortKeyIncrement; - - return db.duplicate(request, { name, metaSortKey, ...patch }); + return db.duplicate(request, { + name, + metaSortKey, + ...patch, + }); } -export function remove(request: Request): Promise { +export function remove(request: Request) { return db.remove(request); } -export async function all(): Promise> { - return db.all(type); +export async function all() { + return db.all(type); } // ~~~~~~~~~~ // @@ -399,9 +434,8 @@ export async function all(): Promise> { /** * Migrate old body (string) to new body (object) * @param request - * @returns {*} */ -function migrateBody(request: Request): Request { +function migrateBody(request: Request) { if (request.body && typeof request.body === 'object') { return request; } @@ -427,12 +461,10 @@ function migrateBody(request: Request): Request { /** * Fix some weird URLs that were caused by an old bug * @param request - * @returns {*} */ -function migrateWeirdUrls(request: Request): Request { +function migrateWeirdUrls(request: Request) { // Some people seem to have requests with URLs that don't have the indexOf // function. This should clear that up. This can be removed at a later date. - if (typeof request.url !== 'string') { request.url = ''; } @@ -443,9 +475,8 @@ function migrateWeirdUrls(request: Request): Request { /** * Ensure the request.authentication.type property is added * @param request - * @returns {*} */ -function migrateAuthType(request: Request): Request { +function migrateAuthType(request: Request) { const isAuthSet = request.authentication && request.authentication.username; if (isAuthSet && !request.authentication.type) { diff --git a/packages/insomnia-app/app/models/response.js b/packages/insomnia-app/app/models/response.ts similarity index 65% rename from packages/insomnia-app/app/models/response.js rename to packages/insomnia-app/app/models/response.ts index bb4dc8d468..1613d7d8c6 100644 --- a/packages/insomnia-app/app/models/response.js +++ b/packages/insomnia-app/app/models/response.ts @@ -1,53 +1,59 @@ -// @flow import type { BaseModel } from './index'; import * as models from './index'; import { Readable } from 'stream'; - import fs from 'fs'; import crypto from 'crypto'; import path from 'path'; import zlib from 'zlib'; import mkdirp from 'mkdirp'; -import * as db from '../common/database'; -import { getDataDirectory } from '../common/misc'; +import { database as db, Query } from '../common/database'; +import { getDataDirectory } from '../common/electron-helpers'; export const name = 'Response'; + export const type = 'Response'; + export const prefix = 'res'; + export const canDuplicate = false; + export const canSync = false; -export type ResponseHeader = { - name: string, - value: string, -}; +export interface ResponseHeader { + name: string; + value: string; +} -export type ResponseTimelineEntry = { - name: string, - value: string, -}; +export interface ResponseTimelineEntry { + name: string; + timestamp: number; + value: string; +} -type BaseResponse = { - environmentId: string | null, - statusCode: number, - statusMessage: string, - httpVersion: string, - contentType: string, - url: string, - bytesRead: number, - bytesContent: number, - elapsedTime: number, - headers: Array, - bodyPath: string, // Actual bodies are stored on the filesystem - timelinePath: string, // Actual timelines are stored on the filesystem - bodyCompression: 'zip' | null | '__NEEDS_MIGRATION__', - error: string, - requestVersionId: string | null, +type Compression = 'zip' | null | '__NEEDS_MIGRATION__' | undefined; +interface BaseResponse { + environmentId: string | null; + statusCode: number; + statusMessage: string; + httpVersion: string; + contentType: string; + url: string; + bytesRead: number; + bytesContent: number; + elapsedTime: number; + headers: Array; + bodyPath: string; + // Actual bodies are stored on the filesystem + timelinePath: string; + // Actual timelines are stored on the filesystem + bodyCompression: Compression; + error: string; + requestVersionId: string | null; // Things from the request - settingStoreCookies: boolean | null, - settingSendCookies: boolean | null, -}; + settingStoreCookies: boolean | null; + settingSendCookies: boolean | null; +} export type Response = BaseModel & BaseResponse; @@ -59,40 +65,42 @@ export function init(): BaseResponse { contentType: '', url: '', bytesRead: 0, - bytesContent: -1, // -1 means that it was legacy and this property didn't exist yet + bytesContent: -1, + // -1 means that it was legacy and this property didn't exist yet elapsedTime: 0, headers: [], - timelinePath: '', // Actual timelines are stored on the filesystem - bodyPath: '', // Actual bodies are stored on the filesystem - bodyCompression: '__NEEDS_MIGRATION__', // For legacy bodies + timelinePath: '', + // Actual timelines are stored on the filesystem + bodyPath: '', + // Actual bodies are stored on the filesystem + bodyCompression: '__NEEDS_MIGRATION__', + // For legacy bodies error: '', requestVersionId: null, - // Things from the request settingStoreCookies: null, settingSendCookies: null, - // Responses sent before environment filtering will have a special value // so they don't show up at all when filtering is on. environmentId: '__LEGACY__', }; } -export async function migrate(doc: Object) { +export async function migrate(doc: Response) { doc = await migrateBodyToFileSystem(doc); doc = await migrateBodyCompression(doc); doc = await migrateTimelineToFileSystem(doc); return doc; } -export function hookDatabaseInit(consoleLog: () => void = console.log) { +export function hookDatabaseInit(consoleLog: typeof console.log = console.log) { consoleLog('[db] Init responses DB'); process.nextTick(async () => { await models.response.cleanDeletedResponses(); }); } -export function hookRemove(doc: Response, consoleLog: () => void = console.log) { +export function hookRemove(doc: Response, consoleLog: typeof console.log = console.log) { fs.unlink(doc.bodyPath, () => { consoleLog(`[response] Delete body ${doc.bodyPath}`); }); @@ -102,16 +110,16 @@ export function hookRemove(doc: Response, consoleLog: () => void = console.log) } export function getById(id: string) { - return db.get(type, id); + return db.get(type, id); } -export async function all(): Promise> { - return db.all(type); +export async function all() { + return db.all(type); } export async function removeForRequest(parentId: string, environmentId?: string | null) { const settings = await models.settings.getOrCreate(); - const query: Object = { + const query: Record = { parentId, }; @@ -135,8 +143,8 @@ async function _findRecentForRequest( requestId: string, environmentId: string | null, limit: number, -): Promise> { - const query: Object = { +) { + const query: Query = { parentId: requestId, }; @@ -145,32 +153,33 @@ async function _findRecentForRequest( query.environmentId = environmentId; } - return db.findMostRecentlyModified(type, query, limit); + return db.findMostRecentlyModified(type, query, limit); } export async function getLatestForRequest( requestId: string, environmentId: string | null, -): Promise { +) { const responses = await _findRecentForRequest(requestId, environmentId, 1); - const response = (responses[0]: ?Response); + const response = responses[0] as Response | null | undefined; return response || null; } -export async function create(patch: Object = {}, maxResponses: number = 20) { +export async function create(patch: Record = {}, maxResponses = 20) { if (!patch.parentId) { throw new Error('New Response missing `parentId`'); } const { parentId } = patch; - // Create request version snapshot const request = await models.request.getById(parentId); const requestVersion = request ? await models.requestVersion.create(request) : null; patch.requestVersionId = requestVersion ? requestVersion._id : null; - // Filter responses by environment if setting is enabled - const query: Object = { parentId }; + const query: Record = { + parentId, + }; + if ( (await models.settings.getOrCreate()).filterResponsesByEnv && patch.hasOwnProperty('environmentId') @@ -179,37 +188,50 @@ export async function create(patch: Object = {}, maxResponses: number = 20) { } // Delete all other responses before creating the new one - const allResponses = await db.findMostRecentlyModified(type, query, Math.max(1, maxResponses)); + const allResponses = await db.findMostRecentlyModified(type, query, Math.max(1, maxResponses)); const recentIds = allResponses.map(r => r._id); - // Remove all that were in the last query, except the first `maxResponses` IDs - await db.removeWhere(type, { ...query, _id: { $nin: recentIds } }); - + await db.removeWhere(type, { + ...query, + _id: { + $nin: recentIds, + }, + }); // Actually create the new response return db.docCreate(type, patch); } export function getLatestByParentId(parentId: string) { - return db.getMostRecentlyModified(type, { parentId }); + return db.getMostRecentlyModified(type, { + parentId, + }); } -export function getBodyStream(response: Object, readFailureValue: ?T): Readable | null | T { +export function getBodyStream( + response: T, + readFailureValue?: TFail | null, +) { return getBodyStreamFromPath(response.bodyPath || '', response.bodyCompression, readFailureValue); } -export function getBodyBuffer(response: Object, readFailureValue: ?T): Buffer | T | null { - return getBodyBufferFromPath(response.bodyPath || '', response.bodyCompression, readFailureValue); -} +export const getBodyBuffer = ( + response?: { bodyPath?: string, bodyCompression?: Compression }, + readFailureValue?: TFail | null, +) => getBodyBufferFromPath( + response?.bodyPath || '', + response?.bodyCompression || null, + readFailureValue, +); -export function getTimeline(response: Object): Array { +export function getTimeline(response: Response) { return getTimelineFromPath(response.timelinePath || ''); } -function getBodyStreamFromPath( +function getBodyStreamFromPath( bodyPath: string, - compression: string | null, - readFailureValue: ?T, -): Readable | null | T { + compression: Compression, + readFailureValue?: TFail | null, +): Readable | null | TFail { // No body, so return empty Buffer if (!bodyPath) { return null; @@ -223,6 +245,7 @@ function getBodyStreamFromPath( } const readStream = fs.createReadStream(bodyPath); + if (compression === 'zip') { return readStream.pipe(zlib.createGunzip()); } else { @@ -232,9 +255,9 @@ function getBodyStreamFromPath( function getBodyBufferFromPath( bodyPath: string, - compression: string | null, - readFailureValue: ?T, -): Buffer | T | null { + compression: Compression, + readFailureValue?: T | null, +) { // No body, so return empty Buffer if (!bodyPath) { return Buffer.alloc(0); @@ -242,6 +265,7 @@ function getBodyBufferFromPath( try { const rawBuffer = fs.readFileSync(bodyPath); + if (compression === 'zip') { return zlib.gunzipSync(rawBuffer); } else { @@ -253,7 +277,7 @@ function getBodyBufferFromPath( } } -function getTimelineFromPath(timelinePath: string): Array { +function getTimelineFromPath(timelinePath: string) { // No body, so return empty Buffer if (!timelinePath) { return []; @@ -261,20 +285,19 @@ function getTimelineFromPath(timelinePath: string): Array try { const rawBuffer = fs.readFileSync(timelinePath); - return JSON.parse(rawBuffer.toString()); + return JSON.parse(rawBuffer.toString()) as Array; } catch (err) { console.warn('Failed to read response body', err.message); return []; } } -async function migrateBodyToFileSystem(doc: Object) { +async function migrateBodyToFileSystem(doc: Response) { if (doc.hasOwnProperty('body') && doc._id && !doc.bodyPath) { + // @ts-expect-error -- TSCONVERSION previously doc.body and doc.encoding did exist but are now removed, and if they exist we want to migrate away from them const bodyBuffer = Buffer.from(doc.body, doc.encoding || 'utf8'); const dir = path.join(getDataDirectory(), 'responses'); - mkdirp.sync(dir); - const hash = crypto .createHash('md5') .update(bodyBuffer || '') @@ -288,13 +311,16 @@ async function migrateBodyToFileSystem(doc: Object) { console.warn('Failed to write response body to file', err.message); } - return db.docUpdate(doc, { bodyPath, bodyCompression: null }); + return db.docUpdate(doc, { + bodyPath, + bodyCompression: null, + }); } else { return doc; } } -function migrateBodyCompression(doc: Object) { +function migrateBodyCompression(doc: Response) { if (doc.bodyCompression === '__NEEDS_MIGRATION__') { doc.bodyCompression = 'zip'; } @@ -302,11 +328,11 @@ function migrateBodyCompression(doc: Object) { return doc; } -async function migrateTimelineToFileSystem(doc: Object) { +async function migrateTimelineToFileSystem(doc: Response) { if (doc.hasOwnProperty('timeline') && doc._id && !doc.timelinePath) { const dir = path.join(getDataDirectory(), 'responses'); - mkdirp.sync(dir); + // @ts-expect-error -- TSCONVERSION previously doc.timeline did exist but is now removed, and if it exists we want to migrate away from it const timelineStr = JSON.stringify(doc.timeline, null, '\t'); const fsPath = doc.bodyPath + '.timeline'; @@ -316,7 +342,9 @@ async function migrateTimelineToFileSystem(doc: Object) { console.warn('Failed to write response body to file', err.message); } - return db.docUpdate(doc, { timelinePath: fsPath }); + return db.docUpdate(doc, { + timelinePath: fsPath, + }); } else { return doc; } @@ -325,14 +353,15 @@ async function migrateTimelineToFileSystem(doc: Object) { export async function cleanDeletedResponses() { const responsesDir = path.join(getDataDirectory(), 'responses'); mkdirp.sync(responsesDir); - const files = fs.readdirSync(responsesDir); + if (files.length === 0) { return; } - const whitelistFiles = []; - for (const r of await db.all(type)) { + const whitelistFiles: Array = []; + + for (const r of (await db.all(type) || [])) { whitelistFiles.push(r.bodyPath.slice(responsesDir.length + 1)); whitelistFiles.push(r.timelinePath.slice(responsesDir.length + 1)); } diff --git a/packages/insomnia-app/app/models/settings.js b/packages/insomnia-app/app/models/settings.ts similarity index 54% rename from packages/insomnia-app/app/models/settings.js rename to packages/insomnia-app/app/models/settings.ts index 14b6895707..337901c129 100644 --- a/packages/insomnia-app/app/models/settings.js +++ b/packages/insomnia-app/app/models/settings.ts @@ -1,6 +1,5 @@ -// @flow import type { BaseModel } from './index'; -import * as db from '../common/database'; +import { database as db } from '../common/database'; import { getAppDefaultTheme, getAppDefaultLightTheme, @@ -11,70 +10,73 @@ import { import * as hotkeys from '../common/hotkeys'; import type { HttpVersion } from '../common/constants'; -export type PluginConfig = { - disabled: boolean, -}; +export interface PluginConfig { + disabled: boolean; +} -export type PluginConfigMap = { - [string]: PluginConfig, -}; +export type PluginConfigMap = Record; -type BaseSettings = { - autoHideMenuBar: boolean, - autocompleteDelay: number, - deviceId: string | null, - disableHtmlPreviewJs: boolean, - disableResponsePreviewLinks: boolean, - disableUpdateNotification: boolean, - editorFontSize: number, - editorIndentSize: number, - editorIndentWithTabs: boolean, - editorKeyMap: string, - editorLineWrapping: boolean, - enableAnalytics: boolean, - environmentHighlightColorStyle: string, - filterResponsesByEnv: boolean, - followRedirects: boolean, - clearOAuth2SessionOnRestart: boolean, - fontInterface: string | null, - fontMonospace: string | null, - fontSize: number, - fontVariantLigatures: boolean, - forceVerticalLayout: boolean, - hotKeyRegistry: hotkeys.HotKeyRegistry, - httpProxy: string, - httpsProxy: string, - maxHistoryResponses: number, - maxRedirects: number, - maxTimelineDataSizeKB: number, - noProxy: string, - nunjucksPowerUserMode: boolean, - pluginConfig: PluginConfigMap, - pluginPath: string, - preferredHttpVersion: HttpVersion, - proxyEnabled: boolean, - showPasswords: boolean, - theme: string, - autoDetectColorScheme: boolean, - lightTheme: string, - darkTheme: string, - timeout: number, - updateAutomatically: boolean, - updateChannel: string, - useBulkHeaderEditor: boolean, - useBulkParametersEditor: boolean, - validateSSL: boolean, - hasPromptedToMigrateFromDesigner: boolean, - hasPromptedOnboarding: boolean, - hasPromptedAnalytics: boolean, -}; +interface BaseSettings { + autoHideMenuBar: boolean; + autocompleteDelay: number; + deviceId: string | null; + disableHtmlPreviewJs: boolean; + disableResponsePreviewLinks: boolean; + disableUpdateNotification: boolean; + editorFontSize: number; + editorIndentSize: number; + editorIndentWithTabs: boolean; + editorKeyMap: string; + editorLineWrapping: boolean; + enableAnalytics: boolean; + environmentHighlightColorStyle: string; + filterResponsesByEnv: boolean; + followRedirects: boolean; + clearOAuth2SessionOnRestart: boolean; + fontInterface: string | null; + fontMonospace: string | null; + fontSize: number; + fontVariantLigatures: boolean; + forceVerticalLayout: boolean; + hotKeyRegistry: hotkeys.HotKeyRegistry; + httpProxy: string; + httpsProxy: string; + lineWrapping?: boolean; + maxHistoryResponses: number; + maxRedirects: number; + maxTimelineDataSizeKB: number; + noProxy: string; + nunjucksPowerUserMode: boolean; + pluginConfig: PluginConfigMap; + pluginPath: string; + preferredHttpVersion: HttpVersion; + proxyEnabled: boolean; + showPasswords: boolean; + theme: string; + autoDetectColorScheme: boolean; + lightTheme: string; + darkTheme: string; + timeout: number; + updateAutomatically: boolean; + updateChannel: string; + useBulkHeaderEditor: boolean; + useBulkParametersEditor: boolean; + validateSSL: boolean; + hasPromptedToMigrateFromDesigner: boolean; + hasPromptedOnboarding: boolean; + hasPromptedAnalytics: boolean; +} export type Settings = BaseModel & BaseSettings; export const name = 'Settings'; + export const type = 'Settings'; + export const prefix = 'set'; + export const canDuplicate = false; + export const canSync = false; export function init(): BaseSettings { @@ -123,14 +125,11 @@ export function init(): BaseSettings { useBulkHeaderEditor: false, useBulkParametersEditor: false, validateSSL: true, - hasPromptedToMigrateFromDesigner: false, - // Users should only see onboarding during first launch, and anybody updating from an // older version should not see it, so by default this flag is set to true, and is toggled // to false during initialization hasPromptedOnboarding: true, - // Only existing users updating from an older version should see the analytics prompt // So by default this flag is set to false, and is toggled to true during initialization // for new users @@ -138,35 +137,37 @@ export function init(): BaseSettings { }; } -export function migrate(doc: Settings): Settings { +export function migrate(doc: Settings) { doc = migrateEnsureHotKeys(doc); return doc; } -export async function all(patch: $Shape = {}): Promise> { - const settings = await db.all(type); - if (settings.length === 0) { +export async function all() { + const settings = await db.all(type); + + if (settings?.length === 0) { return [await getOrCreate()]; } else { return settings; } } -export async function create(patch: $Shape = {}): Promise { - return db.docCreate(type, patch); +export async function create(patch: Partial = {}) { + return db.docCreate(type, patch); } -export async function update(settings: Settings, patch: $Shape): Promise { - return db.docUpdate(settings, patch); +export async function update(settings: Settings, patch: Partial) { + return db.docUpdate(settings, patch); } -export async function patch(patch: $Shape): Promise { +export async function patch(patch: Partial) { const settings = await getOrCreate(); - return db.docUpdate(settings, patch); + return db.docUpdate(settings, patch); } -export async function getOrCreate(): Promise { - const results = await db.all(type); +export async function getOrCreate() { + const results = await db.all(type) || []; + if (results.length === 0) { return create(); } else { @@ -178,9 +179,6 @@ export async function getOrCreate(): Promise { * Ensure map is updated when new hotkeys are added */ function migrateEnsureHotKeys(settings: Settings): Settings { - settings.hotKeyRegistry = { - ...hotkeys.newDefaultRegistry(), - ...settings.hotKeyRegistry, - }; + settings.hotKeyRegistry = { ...hotkeys.newDefaultRegistry(), ...settings.hotKeyRegistry }; return settings; } diff --git a/packages/insomnia-app/app/models/stats.js b/packages/insomnia-app/app/models/stats.ts similarity index 54% rename from packages/insomnia-app/app/models/stats.js rename to packages/insomnia-app/app/models/stats.ts index f54f699c27..b60b088110 100644 --- a/packages/insomnia-app/app/models/stats.js +++ b/packages/insomnia-app/app/models/stats.ts @@ -1,26 +1,29 @@ -// @flow -import * as db from '../common/database'; +import { database as db } from '../common/database'; import type { BaseModel } from './index'; import type { Workspace } from './workspace'; import type { RequestGroup } from './request-group'; import { isRequest, isGrpcRequest } from './helpers/is-model'; export const name = 'Stats'; + export const type = 'Stats'; + export const prefix = 'sta'; + export const canDuplicate = false; + export const canSync = false; -type BaseStats = { - currentLaunch: number | null, - lastLaunch: number | null, - currentVersion: string | null, - lastVersion: string | null, - launches: number, - createdRequests: number, - deletedRequests: number, - executedRequests: number, -}; +interface BaseStats { + currentLaunch: number | null; + lastLaunch: number | null; + currentVersion: string | null; + lastVersion: string | null; + launches: number; + createdRequests: number; + deletedRequests: number; + executedRequests: number; +} export type Stats = BaseModel & BaseStats; @@ -37,21 +40,22 @@ export function init(): BaseStats { }; } -export function migrate(doc: Stats): Stats { +export function migrate(doc: Stats) { return doc; } -export function create(patch: $Shape = {}): Promise { - return db.docCreate(type, patch); +export function create(patch: Partial = {}) { + return db.docCreate(type, patch); } -export async function update(patch: $Shape): Promise { +export async function update(patch: Partial) { const stats = await get(); - return db.docUpdate(stats, patch); + return db.docUpdate(stats, patch); } -export async function get(): Promise { - const results = await db.all(type); +export async function get() { + const results = await db.all(type) || []; + if (results.length === 0) { return create(); } else { @@ -63,35 +67,51 @@ export async function incrementRequestStats({ createdRequests, deletedRequests, executedRequests, -}: $Shape) { +}: Partial) { const stats = await get(); await update({ - ...(createdRequests && { createdRequests: stats.createdRequests + createdRequests }), - ...(deletedRequests && { deletedRequests: stats.deletedRequests + deletedRequests }), - ...(executedRequests && { executedRequests: stats.executedRequests + executedRequests }), + ...(createdRequests && { + createdRequests: stats.createdRequests + createdRequests, + }), + ...(deletedRequests && { + deletedRequests: stats.deletedRequests + deletedRequests, + }), + ...(executedRequests && { + executedRequests: stats.executedRequests + executedRequests, + }), }); } export async function incrementCreatedRequests() { - await incrementRequestStats({ createdRequests: 1 }); + await incrementRequestStats({ + createdRequests: 1, + }); } export async function incrementDeletedRequests() { - await incrementRequestStats({ deletedRequests: 1 }); + await incrementRequestStats({ + deletedRequests: 1, + }); } export async function incrementExecutedRequests() { - await incrementRequestStats({ executedRequests: 1 }); + await incrementRequestStats({ + executedRequests: 1, + }); } export async function incrementCreatedRequestsForDescendents(doc: Workspace | RequestGroup) { const docs = await db.withDescendants(doc); const requests = docs.filter(doc => isRequest(doc) || isGrpcRequest(doc)); - await incrementRequestStats({ createdRequests: requests.length }); + await incrementRequestStats({ + createdRequests: requests.length, + }); } export async function incrementDeletedRequestsForDescendents(doc: Workspace | RequestGroup) { const docs = await db.withDescendants(doc); const requests = docs.filter(doc => isRequest(doc) || isGrpcRequest(doc)); - await incrementRequestStats({ deletedRequests: requests.length }); + await incrementRequestStats({ + deletedRequests: requests.length, + }); } diff --git a/packages/insomnia-app/app/models/unit-test-result.js b/packages/insomnia-app/app/models/unit-test-result.ts similarity index 62% rename from packages/insomnia-app/app/models/unit-test-result.js rename to packages/insomnia-app/app/models/unit-test-result.ts index 9cfbd235fe..8f88553888 100644 --- a/packages/insomnia-app/app/models/unit-test-result.js +++ b/packages/insomnia-app/app/models/unit-test-result.ts @@ -1,16 +1,19 @@ -// @flow -import * as db from '../common/database'; +import { database as db } from '../common/database'; import type { BaseModel } from './index'; export const name = 'Unit Test Result'; + export const type = 'UnitTestResult'; + export const prefix = 'utr'; + export const canDuplicate = false; + export const canSync = false; -type BaseUnitTestResult = { - results: Object, -}; +interface BaseUnitTestResult { + results: Record; +} export type UnitTestResult = BaseModel & BaseUnitTestResult; @@ -20,11 +23,11 @@ export function init() { }; } -export function migrate(doc: UnitTestResult): UnitTestResult { +export function migrate(doc: UnitTestResult) { return doc; } -export function create(patch: $Shape = {}) { +export function create(patch: Partial = {}) { if (!patch.parentId) { throw new Error('New UnitTestResult missing `parentId` ' + JSON.stringify(patch)); } @@ -32,7 +35,7 @@ export function create(patch: $Shape = {}) { return db.docCreate(type, patch); } -export function update(unitTest: UnitTestResult, patch: $Shape) { +export function update(unitTest: UnitTestResult, patch: Partial) { return db.docUpdate(unitTest, patch); } @@ -40,6 +43,6 @@ export function getByParentId(parentId: string) { return db.getWhere(type, { parentId }); } -export function all(): Promise> { - return db.all(type); +export function all() { + return db.all(type); } diff --git a/packages/insomnia-app/app/models/unit-test-suite.js b/packages/insomnia-app/app/models/unit-test-suite.js deleted file mode 100644 index c0186c66f9..0000000000 --- a/packages/insomnia-app/app/models/unit-test-suite.js +++ /dev/null @@ -1,52 +0,0 @@ -// @flow -import * as db from '../common/database'; -import type { BaseModel } from './index'; - -export const name = 'Unit Test Suite'; -export const type = 'UnitTestSuite'; -export const prefix = 'uts'; -export const canDuplicate = true; -export const canSync = true; - -type BaseUnitTestSuite = { - name: string, -}; - -export type UnitTestSuite = BaseModel & BaseUnitTestSuite; - -export function init() { - return { - name: 'My Test', - }; -} - -export function migrate(doc: UnitTestSuite): UnitTestSuite { - return doc; -} - -export function create(patch: $Shape = {}) { - if (!patch.parentId) { - throw new Error('New UnitTestSuite missing `parentId` ' + JSON.stringify(patch)); - } - - return db.docCreate(type, patch); -} - -export function update( - unitTestSuite: UnitTestSuite, - patch: $Shape = {}, -): Promise { - return db.docUpdate(unitTestSuite, patch); -} - -export function remove(unitTestSuite: UnitTestSuite): Promise { - return db.remove(unitTestSuite); -} - -export function getByParentId(parentId: string): Promise { - return db.getWhere(type, { parentId }); -} - -export function all(): Promise> { - return db.all(type); -} diff --git a/packages/insomnia-app/app/models/unit-test-suite.ts b/packages/insomnia-app/app/models/unit-test-suite.ts new file mode 100644 index 0000000000..8670c9c592 --- /dev/null +++ b/packages/insomnia-app/app/models/unit-test-suite.ts @@ -0,0 +1,51 @@ +import { database as db } from '../common/database'; +import type { BaseModel } from './index'; + +export const name = 'Unit Test Suite'; + +export const type = 'UnitTestSuite'; + +export const prefix = 'uts'; + +export const canDuplicate = true; + +export const canSync = true; +interface BaseUnitTestSuite { + name: string; +} + +export type UnitTestSuite = BaseModel & BaseUnitTestSuite; + +export function init() { + return { + name: 'My Test', + }; +} + +export function migrate(doc: UnitTestSuite) { + return doc; +} + +export function create(patch: Partial = {}) { + if (!patch.parentId) { + throw new Error('New UnitTestSuite missing `parentId` ' + JSON.stringify(patch)); + } + + return db.docCreate(type, patch); +} + +export function update(unitTestSuite: UnitTestSuite, patch: Partial = {}) { + return db.docUpdate(unitTestSuite, patch); +} + +export function remove(unitTestSuite: UnitTestSuite) { + return db.remove(unitTestSuite); +} + +export function getByParentId(parentId: string) { + return db.getWhere(type, { parentId }); +} + +export function all() { + return db.all(type); +} diff --git a/packages/insomnia-app/app/models/unit-test.js b/packages/insomnia-app/app/models/unit-test.js deleted file mode 100644 index d27bc6c68b..0000000000 --- a/packages/insomnia-app/app/models/unit-test.js +++ /dev/null @@ -1,53 +0,0 @@ -// @flow -import * as db from '../common/database'; -import type { BaseModel } from './index'; - -export const name = 'Unit Test'; -export const type = 'UnitTest'; -export const prefix = 'ut'; -export const canDuplicate = true; -export const canSync = true; - -type BaseUnitTest = { - name: string, - code: string, - requestId: string | null, -}; - -export type UnitTest = BaseModel & BaseUnitTest; - -export function init() { - return { - requestId: null, - name: 'My Test', - code: '', - }; -} - -export function migrate(doc: UnitTest): UnitTest { - return doc; -} - -export function create(patch: $Shape = {}): Promise { - if (!patch.parentId) { - throw new Error('New UnitTest missing `parentId` ' + JSON.stringify(patch)); - } - - return db.docCreate(type, patch); -} - -export function remove(unitTest: UnitTest): Promise { - return db.remove(unitTest); -} - -export function update(unitTest: UnitTest, patch: $Shape = {}): Promise { - return db.docUpdate(unitTest, patch); -} - -export function getByParentId(parentId: string): Promise { - return db.getWhere(type, { parentId }); -} - -export function all(): Promise> { - return db.all(type); -} diff --git a/packages/insomnia-app/app/models/unit-test.ts b/packages/insomnia-app/app/models/unit-test.ts new file mode 100644 index 0000000000..87b01bd6bb --- /dev/null +++ b/packages/insomnia-app/app/models/unit-test.ts @@ -0,0 +1,55 @@ +import { database as db } from '../common/database'; +import type { BaseModel } from './index'; + +export const name = 'Unit Test'; + +export const type = 'UnitTest'; + +export const prefix = 'ut'; + +export const canDuplicate = true; + +export const canSync = true; +interface BaseUnitTest { + name: string; + code: string; + requestId: string | null; +} + +export type UnitTest = BaseModel & BaseUnitTest; + +export function init() { + return { + requestId: null, + name: 'My Test', + code: '', + }; +} + +export function migrate(doc: UnitTest) { + return doc; +} + +export function create(patch: Partial = {}) { + if (!patch.parentId) { + throw new Error('New UnitTest missing `parentId` ' + JSON.stringify(patch)); + } + + return db.docCreate(type, patch); +} + +export function remove(unitTest: UnitTest) { + return db.remove(unitTest); +} + +export function update(unitTest: UnitTest, patch: Partial = {}) { + return db.docUpdate(unitTest, patch); +} + +export function getByParentId(parentId: string) { + return db.getWhere(type, { parentId }); +} + +export function all() { + return db.all(type); +} diff --git a/packages/insomnia-app/app/models/workspace-meta.js b/packages/insomnia-app/app/models/workspace-meta.js deleted file mode 100644 index 6def1f754f..0000000000 --- a/packages/insomnia-app/app/models/workspace-meta.js +++ /dev/null @@ -1,110 +0,0 @@ -// @flow -import type { BaseModel } from './index'; -import * as db from '../common/database'; -import { - DEFAULT_SIDEBAR_WIDTH, - DEFAULT_PANE_WIDTH, - DEFAULT_PANE_HEIGHT, - ACTIVITY_DEBUG, - DEPRECATED_ACTIVITY_INSOMNIA, -} from '../common/constants'; - -export const name = 'Workspace Meta'; -export const type = 'WorkspaceMeta'; -export const prefix = 'wrkm'; -export const canDuplicate = false; -export const canSync = false; - -type BaseWorkspaceMeta = { - activeActivity: string | null, - activeEnvironmentId: string | null, - activeRequestId: string | null, - activeUnitTestSuiteId: string | null, - cachedGitLastAuthor: string | null, - cachedGitLastCommitTime: number | null, - cachedGitRepositoryBranch: string | null, - gitRepositoryId: string | null, - hasSeen: boolean, - paneHeight: number, - paneWidth: number, - previewHidden: boolean, - sidebarFilter: string, - sidebarHidden: boolean, - sidebarWidth: number, -}; - -export type WorkspaceMeta = BaseWorkspaceMeta & BaseModel; - -export function init(): BaseWorkspaceMeta { - return { - activeActivity: null, - activeEnvironmentId: null, - activeRequestId: null, - activeUnitTestSuiteId: null, - cachedGitLastAuthor: null, - cachedGitLastCommitTime: null, - cachedGitRepositoryBranch: null, - gitRepositoryId: null, - hasSeen: true, - paneHeight: DEFAULT_PANE_HEIGHT, - paneWidth: DEFAULT_PANE_WIDTH, - parentId: null, - previewHidden: false, - sidebarFilter: '', - sidebarHidden: false, - sidebarWidth: DEFAULT_SIDEBAR_WIDTH, - }; -} - -export function migrate(doc: WorkspaceMeta): WorkspaceMeta { - doc = _migrateInsomniaActivity(doc); - return doc; -} - -function _migrateInsomniaActivity(doc: WorkspaceMeta): WorkspaceMeta { - if (doc.activeActivity === DEPRECATED_ACTIVITY_INSOMNIA) { - doc.activeActivity = ACTIVITY_DEBUG; - } - - return doc; -} - -export function create(patch: $Shape = {}): Promise { - if (!patch.parentId) { - throw new Error(`New WorkspaceMeta missing parentId ${JSON.stringify(patch)}`); - } - - return db.docCreate(type, patch); -} - -export function update( - workspaceMeta: WorkspaceMeta, - patch: $Shape = {}, -): Promise { - return db.docUpdate(workspaceMeta, patch); -} - -export async function updateByParentId( - workspaceId: string, - patch: $Shape = {}, -): Promise { - const meta = await getByParentId(workspaceId); - return db.docUpdate(meta, patch); -} - -export async function getByParentId(parentId: string): Promise { - return db.getWhere(type, { parentId }); -} - -export async function getByGitRepositoryId(gitRepositoryId: string): Promise { - return db.getWhere(type, { gitRepositoryId }); -} - -export async function getOrCreateByParentId(parentId: string): Promise { - const doc = await getByParentId(parentId); - return doc || this.create({ parentId }); -} - -export function all(): Promise> { - return db.all(type); -} diff --git a/packages/insomnia-app/app/models/workspace-meta.ts b/packages/insomnia-app/app/models/workspace-meta.ts new file mode 100644 index 0000000000..60c8be0cb9 --- /dev/null +++ b/packages/insomnia-app/app/models/workspace-meta.ts @@ -0,0 +1,110 @@ +import type { BaseModel } from './index'; +import { database as db } from '../common/database'; +import { + DEFAULT_SIDEBAR_WIDTH, + DEFAULT_PANE_WIDTH, + DEFAULT_PANE_HEIGHT, + ACTIVITY_DEBUG, + DEPRECATED_ACTIVITY_INSOMNIA, +} from '../common/constants'; + +export const name = 'Workspace Meta'; + +export const type = 'WorkspaceMeta'; + +export const prefix = 'wrkm'; + +export const canDuplicate = false; + +export const canSync = false; + +interface BaseWorkspaceMeta { + activeActivity: string | null; + activeEnvironmentId: string | null; + activeRequestId: string | null; + activeUnitTestSuiteId: string | null; + cachedGitLastAuthor: string | null; + cachedGitLastCommitTime: number | null; + cachedGitRepositoryBranch: string | null; + gitRepositoryId: string | null; + hasSeen: boolean; + paneHeight: number; + paneWidth: number; + parentId: string | null; + previewHidden: boolean; + sidebarFilter: string; + sidebarHidden: boolean; + sidebarWidth: number; +} + +export type WorkspaceMeta = BaseWorkspaceMeta & BaseModel; + +export function init(): BaseWorkspaceMeta { + return { + activeActivity: null, + activeEnvironmentId: null, + activeRequestId: null, + activeUnitTestSuiteId: null, + cachedGitLastAuthor: null, + cachedGitLastCommitTime: null, + cachedGitRepositoryBranch: null, + gitRepositoryId: null, + hasSeen: true, + paneHeight: DEFAULT_PANE_HEIGHT, + paneWidth: DEFAULT_PANE_WIDTH, + parentId: null, + previewHidden: false, + sidebarFilter: '', + sidebarHidden: false, + sidebarWidth: DEFAULT_SIDEBAR_WIDTH, + }; +} + +export function migrate(doc: WorkspaceMeta) { + doc = _migrateInsomniaActivity(doc); + return doc; +} + +function _migrateInsomniaActivity(doc: WorkspaceMeta) { + if (doc.activeActivity === DEPRECATED_ACTIVITY_INSOMNIA) { + doc.activeActivity = ACTIVITY_DEBUG; + } + + return doc; +} + +export function create(patch: Partial = {}) { + if (!patch.parentId) { + throw new Error(`New WorkspaceMeta missing parentId ${JSON.stringify(patch)}`); + } + + return db.docCreate(type, patch); +} + +export function update(workspaceMeta: WorkspaceMeta, patch: Partial = {}) { + return db.docUpdate(workspaceMeta, patch); +} + +export async function updateByParentId(workspaceId: string, patch: Partial = {}) { + const meta = await getByParentId(workspaceId); + // @ts-expect-error -- TSCONVERSION appears to be a genuine error not previously caught by Flow + return db.docUpdate(meta, patch); +} + +export async function getByParentId(parentId: string) { + return db.getWhere(type, { parentId }); +} + +export async function getByGitRepositoryId(gitRepositoryId: string) { + // @ts-expect-error -- TSCONVERSION needs generic for query + return db.getWhere(type, { gitRepositoryId }); +} + +export async function getOrCreateByParentId(parentId: string) { + const doc = await getByParentId(parentId); + return doc || create({ parentId }); +} + +export function all() { + return db.all(type); +} diff --git a/packages/insomnia-app/app/models/workspace.js b/packages/insomnia-app/app/models/workspace.ts similarity index 68% rename from packages/insomnia-app/app/models/workspace.js rename to packages/insomnia-app/app/models/workspace.ts index 35929165c1..60f7752eca 100644 --- a/packages/insomnia-app/app/models/workspace.js +++ b/packages/insomnia-app/app/models/workspace.ts @@ -1,28 +1,33 @@ -// @flow import type { BaseModel } from './index'; import * as models from './index'; -import * as db from '../common/database'; +import { database as db } from '../common/database'; import { getAppName } from '../common/constants'; import { strings } from '../common/strings'; +import { ValueOf } from 'type-fest'; export const name = 'Workspace'; + export const type = 'Workspace'; + export const prefix = 'wrk'; + export const canDuplicate = true; + export const canSync = true; export const WorkspaceScopeKeys = { design: 'design', collection: 'collection', -}; +} as const; -export type WorkspaceScope = $Keys; +export type WorkspaceScope = ValueOf; -type BaseWorkspace = { - name: string, - description: string, - scope: WorkspaceScope, -}; +interface BaseWorkspace { + name: string; + description: string; + scope: WorkspaceScope; + certificates?: any; +} export type Workspace = BaseModel & BaseWorkspace; @@ -34,28 +39,33 @@ export function init() { }; } -export async function migrate(doc: Workspace): Promise { +export async function migrate(doc: Workspace) { doc = await _migrateExtractClientCertificates(doc); doc = await _migrateEnsureName(doc); - await models.apiSpec.getOrCreateForParentId(doc._id, { fileName: doc.name }); + await models.apiSpec.getOrCreateForParentId(doc._id, { + fileName: doc.name, + }); doc = _migrateScope(doc); return doc; } -export function getById(id: string): Promise { - return db.get(type, id); +export function getById(id?: string) { + return db.get(type, id); } -export async function create(patch: $Shape = {}): Promise { - return db.docCreate(type, patch); +export async function create(patch: Partial = {}) { + return db.docCreate(type, patch); } -export async function all(): Promise> { - const workspaces = await db.all(type); +export async function all() { + const workspaces = await db.all(type) || []; if (workspaces.length === 0) { // Create default workspace - await create({ name: getAppName(), scope: WorkspaceScopeKeys.collection }); + await create({ + name: getAppName(), + scope: WorkspaceScopeKeys.collection, + }); return all(); } else { return workspaces; @@ -66,16 +76,17 @@ export function count() { return db.count(type); } -export function update(workspace: Workspace, patch: $Shape): Promise { +export function update(workspace: Workspace, patch: Partial) { return db.docUpdate(workspace, patch); } -export function remove(workspace: Workspace): Promise { +export function remove(workspace: Workspace) { return db.remove(workspace); } -async function _migrateExtractClientCertificates(workspace: Workspace): Promise { - const certificates = (workspace: Object).certificates || null; +async function _migrateExtractClientCertificates(workspace: Workspace) { + const certificates = workspace.certificates || null; + if (!Array.isArray(certificates)) { // Already migrated return workspace; @@ -93,12 +104,10 @@ async function _migrateExtractClientCertificates(workspace: Workspace): Promise< }); } - delete (workspace: Object).certificates; - + delete workspace.certificates; // This will remove the now-missing `certificates` property // NOTE: Using db.update so we don't change things like modified time await db.update(workspace); - return workspace; } @@ -107,7 +116,7 @@ async function _migrateExtractClientCertificates(workspace: Workspace): Promise< * this happens (and it causes problems) so this migration will ensure that it is * corrected. */ -async function _migrateEnsureName(workspace: Workspace): Promise { +async function _migrateEnsureName(workspace: Workspace) { if (typeof workspace.name !== 'string') { workspace.name = 'My Workspace'; } @@ -118,7 +127,7 @@ async function _migrateEnsureName(workspace: Workspace): Promise { /** * Ensure workspace scope is set to a valid entry */ -function _migrateScope(workspace: Workspace): Workspace { +function _migrateScope(workspace: Workspace) { if ( workspace.scope === WorkspaceScopeKeys.design || workspace.scope === WorkspaceScopeKeys.collection @@ -128,15 +137,18 @@ function _migrateScope(workspace: Workspace): Workspace { // Translate the old value type OldScopeTypes = 'spec' | 'debug' | 'designer' | null; - switch ((workspace.scope: OldScopeTypes)) { + + switch (workspace.scope as OldScopeTypes) { case 'spec': { workspace.scope = WorkspaceScopeKeys.design; break; } + case 'designer': { workspace.scope = WorkspaceScopeKeys.design; break; } + case 'debug': case null: default: diff --git a/packages/insomnia-app/app/network/__tests__/authentication.test.js b/packages/insomnia-app/app/network/__tests__/authentication.test.ts similarity index 87% rename from packages/insomnia-app/app/network/__tests__/authentication.test.js rename to packages/insomnia-app/app/network/__tests__/authentication.test.ts index 7fd2e6076e..839aaf9771 100644 --- a/packages/insomnia-app/app/network/__tests__/authentication.test.js +++ b/packages/insomnia-app/app/network/__tests__/authentication.test.ts @@ -14,9 +14,12 @@ describe('OAuth 1.0', () => { nonce: 'nonce', timestamp: '1234567890', }; - const request = { url: 'https://insomnia.rest/', method: 'GET', authentication }; + const request = { + url: 'https://insomnia.rest/', + method: 'GET', + authentication, + }; const header = await getAuthHeader(request, 'https://insomnia.rest/'); - expect(header).toEqual({ name: 'Authorization', value: [ @@ -60,9 +63,12 @@ describe('OAuth 1.0', () => { nonce: 'nonce', timestamp: '1234567890', }; - const request = { url: 'https://insomnia.rest/', method: 'GET', authentication }; + const request = { + url: 'https://insomnia.rest/', + method: 'GET', + authentication, + }; const header = await getAuthHeader(request, 'https://insomnia.rest/'); - expect(header).toEqual({ name: 'Authorization', value: [ @@ -85,9 +91,12 @@ describe('OAuth 1.0', () => { consumerSecret: 'consumerSecret', signatureMethod: 'HMAC-SHA1', }; - const request = { url: 'https://insomnia.rest/', method: 'GET', authentication }; + const request = { + url: 'https://insomnia.rest/', + method: 'GET', + authentication, + }; const header = await getAuthHeader(request, 'https://insomnia.rest/'); - expect(header.name).toBe('Authorization'); expect(header.value).toMatch( new RegExp( @@ -107,16 +116,28 @@ describe('OAuth 1.0', () => { describe('_buildBearerHeader()', () => { it('uses default prefix', () => { const result = _buildBearerHeader('token', ''); - expect(result).toEqual({ name: 'Authorization', value: 'Bearer token' }); + + expect(result).toEqual({ + name: 'Authorization', + value: 'Bearer token', + }); }); it('uses specified prefix', () => { const result = _buildBearerHeader('token', 'custom'); - expect(result).toEqual({ name: 'Authorization', value: 'custom token' }); + + expect(result).toEqual({ + name: 'Authorization', + value: 'custom token', + }); }); it('uses no prefix', () => { const result = _buildBearerHeader('token', 'NO_PREFIX'); - expect(result).toEqual({ name: 'Authorization', value: 'token' }); + + expect(result).toEqual({ + name: 'Authorization', + value: 'token', + }); }); }); diff --git a/packages/insomnia-app/app/network/__tests__/certificate-url-parse.test.js b/packages/insomnia-app/app/network/__tests__/certificate-url-parse.test.ts similarity index 99% rename from packages/insomnia-app/app/network/__tests__/certificate-url-parse.test.js rename to packages/insomnia-app/app/network/__tests__/certificate-url-parse.test.ts index b69dfd229e..0bc9ec85e0 100644 --- a/packages/insomnia-app/app/network/__tests__/certificate-url-parse.test.js +++ b/packages/insomnia-app/app/network/__tests__/certificate-url-parse.test.ts @@ -4,6 +4,7 @@ import { globalBeforeEach } from '../../__jest__/before-each'; describe('certificateUrlParse', () => { beforeEach(globalBeforeEach); + it('should return the result of url.parse if no wildcard paths are supplied', () => { const url = 'https://www.example.org:80/some/resources?query=1&other=2#myfragment'; const expected = urlParse(url); @@ -17,7 +18,6 @@ describe('certificateUrlParse', () => { const path = '/some/resources'; const query = 'query=1&other=2'; const fragment = 'myfragment'; - const url = `${protocol}://${host}:${port}${path}?${query}#${fragment}`; expect(certificateUrlParse(url).hostname).toEqual(host); }); @@ -29,7 +29,6 @@ describe('certificateUrlParse', () => { const path = '/some/resources'; const query = 'query=1&other=2'; const fragment = 'myfragment'; - const url = `${protocol}://${host}:${port}${path}?${query}#${fragment}`; expect(certificateUrlParse(url).hostname).toEqual(host); }); @@ -41,7 +40,6 @@ describe('certificateUrlParse', () => { const path = '/some/resources'; const query = 'query=1&other=2'; const fragment = 'myfragment'; - const url = `${protocol}://${host}:${port}${path}?${query}#${fragment}`; expect(certificateUrlParse(url).hostname).toEqual(host); }); @@ -53,7 +51,6 @@ describe('certificateUrlParse', () => { const path = '/some/resources'; const query = 'query=1&other=2'; const fragment = 'myfragment'; - const url = `${protocol}://${host}:${port}${path}?${query}#${fragment}`; expect(certificateUrlParse(url).hostname).toEqual(host); }); @@ -63,7 +60,6 @@ describe('certificateUrlParse', () => { const path = '/some/resources'; const query = 'query=1&other=2'; const fragment = 'myfragment'; - const url = `${host}${path}?${query}#${fragment}`; expect(certificateUrlParse(url).hostname).toEqual(null); }); @@ -76,7 +72,6 @@ describe('certificateUrlParse', () => { const path = '/some/resources'; const query = 'query=1&other=2'; const fragment = 'myfragment'; - const url = `${protocol}://${user}:${password}@${host}${path}?${query}#${fragment}`; expect(certificateUrlParse(url).hostname).toEqual(host); }); @@ -87,7 +82,6 @@ describe('certificateUrlParse', () => { const path = '/@some/resources'; const query = 'query=1&other=2'; const fragment = 'myfragment'; - const url = `${protocol}://${host}${path}?${query}#${fragment}`; expect(certificateUrlParse(url).hostname).toEqual(host); }); @@ -102,7 +96,6 @@ describe('certificateUrlParse', () => { const path = '/some/resources'; const query = 'query=1&other=2'; const fragment = 'myfragment'; - const url = `${protocol}://${user}:${password}@${host}:${port}${path}?${query}#${fragment}`; const nonWildcardUrl = `${protocol}://${user}:${password}@${nonWildcardHost}:${port}${path}?${query}#${fragment}`; const expected = urlParse(nonWildcardUrl); @@ -119,7 +112,6 @@ describe('certificateUrlParse', () => { const path = '/some/resources'; const query = 'query=1&other=2'; const fragment = 'myfragment'; - const url = `${protocol}://${host}:${port}${path}?${query}#${fragment}`; expect(certificateUrlParse(url).port).toEqual(port); }); diff --git a/packages/insomnia-app/app/network/__tests__/multipart.test.js b/packages/insomnia-app/app/network/__tests__/multipart.test.ts similarity index 78% rename from packages/insomnia-app/app/network/__tests__/multipart.test.js rename to packages/insomnia-app/app/network/__tests__/multipart.test.ts index 6db314a116..2042584a09 100644 --- a/packages/insomnia-app/app/network/__tests__/multipart.test.js +++ b/packages/insomnia-app/app/network/__tests__/multipart.test.ts @@ -8,10 +8,15 @@ describe('buildMultipart()', () => { it('builds a simple request', async () => { const { filePath, boundary, contentLength } = await buildMultipart([ - { name: 'foo', value: 'bar' }, - { name: 'multi-line', value: 'Hello\nWorld!' }, + { + name: 'foo', + value: 'bar', + }, + { + name: 'multi-line', + value: 'Hello\nWorld!', + }, ]); - expect(boundary).toBe(DEFAULT_BOUNDARY); expect(contentLength).toBe(189); expect(fs.readFileSync(filePath, 'utf8')).toBe( @@ -32,11 +37,21 @@ describe('buildMultipart()', () => { it('builds a multiline request with content-type', async () => { const { filePath, boundary, contentLength } = await buildMultipart([ - { name: 'foo', value: 'bar' }, - { name: 'json', value: '{"hello": "world"}', multiline: 'application/json' }, - { name: 'text', value: 'text', multiline: true }, + { + name: 'foo', + value: 'bar', + }, + { + name: 'json', + value: '{"hello": "world"}', + multiline: 'application/json', + }, + { + name: 'text', + value: 'text', + multiline: true, + }, ]); - expect(boundary).toBe(DEFAULT_BOUNDARY); expect(contentLength).toBe(297); expect(fs.readFileSync(filePath, 'utf8')).toBe( @@ -63,11 +78,20 @@ describe('buildMultipart()', () => { it('builds with file', async () => { const fileName = path.resolve(path.join(__dirname, './testfile.txt')); const { filePath, boundary, contentLength } = await buildMultipart([ - { name: 'foo', value: 'bar' }, - { name: 'file', type: 'file', fileName: fileName }, - { name: 'baz', value: 'qux' }, + { + name: 'foo', + value: 'bar', + }, + { + name: 'file', + type: 'file', + fileName: fileName, + }, + { + name: 'baz', + value: 'qux', + }, ]); - expect(boundary).toBe(DEFAULT_BOUNDARY); expect(contentLength).toBe(322); expect(fs.readFileSync(filePath, 'utf8')).toBe( @@ -93,12 +117,22 @@ describe('buildMultipart()', () => { it('skips entries with no name or value', async () => { const { filePath, boundary, contentLength } = await buildMultipart([ - { value: 'bar' }, - { name: 'foo' }, - { name: '', value: '' }, - { name: '', type: 'file', fileName: '' }, + { + value: 'bar', + }, + { + name: 'foo', + }, + { + name: '', + value: '', + }, + { + name: '', + type: 'file', + fileName: '', + }, ]); - expect(boundary).toBe(DEFAULT_BOUNDARY); expect(contentLength).toBe(167); expect(fs.readFileSync(filePath, 'utf8')).toBe( diff --git a/packages/insomnia-app/app/network/__tests__/network.test.js b/packages/insomnia-app/app/network/__tests__/network.test.ts similarity index 83% rename from packages/insomnia-app/app/network/__tests__/network.test.js rename to packages/insomnia-app/app/network/__tests__/network.test.ts index 5b865b310e..7ac62dbc95 100644 --- a/packages/insomnia-app/app/network/__tests__/network.test.js +++ b/packages/insomnia-app/app/network/__tests__/network.test.ts @@ -16,13 +16,13 @@ import { import { filterHeaders } from '../../common/misc'; import { globalBeforeEach } from '../../__jest__/before-each'; import { DEFAULT_BOUNDARY } from '../multipart'; - const CONTEXT = {}; const getRenderedRequest = async (...args) => (await getRenderedRequestAndContext(...args)).request; describe('actuallySend()', () => { beforeEach(globalBeforeEach); + it('sends a generic request', async () => { const workspace = await models.workspace.create(); const settings = await models.settings.create(); @@ -48,25 +48,39 @@ describe('actuallySend()', () => { lastAccessed: new Date('2096-10-05T04:40:49.505Z'), }, ]; - const cookieJar = await models.cookieJar.getOrCreateForParentId(workspace._id); await models.cookieJar.update(cookieJar, { parentId: workspace._id, cookies, }); - const request = Object.assign(models.request.init(), { _id: 'req_123', parentId: workspace._id, headers: [ - { name: 'Content-Type', value: 'application/json' }, - { name: 'Empty', value: '' }, + { + name: 'Content-Type', + value: 'application/json', + }, + { + name: 'Empty', + value: '', + }, + ], + parameters: [ + { + name: 'foo bar', + value: 'hello&world', + }, ], - parameters: [{ name: 'foo bar', value: 'hello&world' }], method: 'POST', body: { mimeType: CONTENT_TYPE_FORM_URLENCODED, - params: [{ name: 'foo', value: 'bar' }], + params: [ + { + name: 'foo', + value: 'bar', + }, + ], }, url: 'http://localhost', authentication: { @@ -75,7 +89,6 @@ describe('actuallySend()', () => { password: 'pass', }, }); - const renderedRequest = await getRenderedRequest(request); const response = await networkUtils._actuallySend( renderedRequest, @@ -83,7 +96,6 @@ describe('actuallySend()', () => { workspace, settings, ); - const bodyBuffer = models.response.getBodyBuffer(response); const body = JSON.parse(bodyBuffer); expect(body).toEqual({ @@ -126,19 +138,32 @@ describe('actuallySend()', () => { const request = Object.assign(models.request.init(), { _id: 'req_123', parentId: workspace._id, - headers: [{ name: 'Content-Type', value: CONTENT_TYPE_FORM_URLENCODED }], + headers: [ + { + name: 'Content-Type', + value: CONTENT_TYPE_FORM_URLENCODED, + }, + ], method: 'POST', body: { mimeType: CONTENT_TYPE_FORM_URLENCODED, params: [ - { name: 'foo', value: 'bar' }, - { name: 'bar', value: '' }, - { name: '', value: 'value' }, + { + name: 'foo', + value: 'bar', + }, + { + name: 'bar', + value: '', + }, + { + name: '', + value: 'value', + }, ], }, url: 'http://localhost', }); - const renderedRequest = await getRenderedRequest(request); const response = await networkUtils._actuallySend( renderedRequest, @@ -146,7 +171,6 @@ describe('actuallySend()', () => { workspace, settings, ); - const bodyBuffer = models.response.getBodyBuffer(response); const body = JSON.parse(bodyBuffer); expect(body).toEqual({ @@ -202,21 +226,34 @@ describe('actuallySend()', () => { lastAccessed: new Date('2096-10-05T04:40:49.505Z'), }, ]; - await models.cookieJar.create({ parentId: workspace._id, cookies, }); - const request = Object.assign(models.request.init(), { _id: 'req_123', parentId: workspace._id, - headers: [{ name: 'Content-Type', value: 'application/json' }], - parameters: [{ name: 'foo bar', value: 'hello&world' }], + headers: [ + { + name: 'Content-Type', + value: 'application/json', + }, + ], + parameters: [ + { + name: 'foo bar', + value: 'hello&world', + }, + ], method: 'GET', body: { mimeType: CONTENT_TYPE_FORM_URLENCODED, - params: [{ name: 'foo', value: 'bar' }], + params: [ + { + name: 'foo', + value: 'bar', + }, + ], }, url: 'http://localhost', authentication: { @@ -227,7 +264,6 @@ describe('actuallySend()', () => { settingStoreCookies: false, settingSendCookies: false, }); - const renderedRequest = await getRenderedRequest(request); const response = await networkUtils._actuallySend( renderedRequest, @@ -235,7 +271,6 @@ describe('actuallySend()', () => { workspace, settings, ); - const bodyBuffer = models.response.getBodyBuffer(response); const body = JSON.parse(bodyBuffer); expect(body).toEqual({ @@ -269,18 +304,26 @@ describe('actuallySend()', () => { it('sends a file', async () => { const workspace = await models.workspace.create(); const settings = await models.settings.create(); - await models.cookieJar.create({ parentId: workspace._id }); + await models.cookieJar.create({ + parentId: workspace._id, + }); const fileName = pathResolve(pathJoin(__dirname, './testfile.txt')); - const request = Object.assign(models.request.init(), { _id: 'req_123', parentId: workspace._id, - headers: [{ name: 'Content-Type', value: 'application/octet-stream' }], + headers: [ + { + name: 'Content-Type', + value: 'application/octet-stream', + }, + ], url: 'http://localhost', method: 'POST', - body: { mimeType: CONTENT_TYPE_FILE, fileName }, + body: { + mimeType: CONTENT_TYPE_FILE, + fileName, + }, }); - const renderedRequest = await getRenderedRequest(request); const response = await networkUtils._actuallySend( renderedRequest, @@ -288,10 +331,8 @@ describe('actuallySend()', () => { workspace, settings, ); - const bodyBuffer = models.response.getBodyBuffer(response); const body = JSON.parse(bodyBuffer); - expect(body).toEqual({ meta: {}, features: { @@ -326,28 +367,43 @@ describe('actuallySend()', () => { it('sends multipart form data', async () => { const workspace = await models.workspace.create(); const settings = await models.settings.create(); - await models.cookieJar.create({ parentId: workspace._id }); + await models.cookieJar.create({ + parentId: workspace._id, + }); const fileName = pathResolve(pathJoin(__dirname, './testfile.txt')); - const request = Object.assign(models.request.init(), { _id: 'req_123', parentId: workspace._id, - headers: [{ name: 'Content-Type', value: 'multipart/form-data' }], + headers: [ + { + name: 'Content-Type', + value: 'multipart/form-data', + }, + ], url: 'http://localhost', method: 'POST', body: { mimeType: CONTENT_TYPE_FORM_DATA, params: [ // Should ignore value and send the file since type is set to file - { name: 'foo', fileName: fileName, value: 'bar', type: 'file' }, - - // Some extra params - { name: 'a', value: 'AA' }, - { name: 'baz', value: 'qux', disabled: true }, + { + name: 'foo', + fileName: fileName, + value: 'bar', + type: 'file', + }, // Some extra params + { + name: 'a', + value: 'AA', + }, + { + name: 'baz', + value: 'qux', + disabled: true, + }, ], }, }); - const renderedRequest = await getRenderedRequest(request); const response = await networkUtils._actuallySend( renderedRequest, @@ -403,14 +459,12 @@ describe('actuallySend()', () => { it('uses unix socket', async () => { const workspace = await models.workspace.create(); const settings = await models.settings.create(); - const request = Object.assign(models.request.init(), { _id: 'req_123', parentId: workspace._id, url: 'http://unix:/my/socket:/my/path', method: 'GET', }); - const renderedRequest = await getRenderedRequest(request); const response = await networkUtils._actuallySend( renderedRequest, @@ -418,7 +472,6 @@ describe('actuallySend()', () => { workspace, settings, ); - const bodyBuffer = models.response.getBodyBuffer(response); const body = JSON.parse(bodyBuffer); expect(body).toEqual({ @@ -446,14 +499,12 @@ describe('actuallySend()', () => { it('uses works with HEAD', async () => { const workspace = await models.workspace.create(); const settings = await models.settings.create(); - const request = Object.assign(models.request.init(), { _id: 'req_123', parentId: workspace._id, url: 'http://localhost:3000/foo/bar', method: 'HEAD', }); - const renderedRequest = await getRenderedRequest(request); const response = await networkUtils._actuallySend( renderedRequest, @@ -461,7 +512,6 @@ describe('actuallySend()', () => { workspace, settings, ); - const bodyBuffer = models.response.getBodyBuffer(response); const body = JSON.parse(bodyBuffer); expect(body).toEqual({ @@ -488,14 +538,12 @@ describe('actuallySend()', () => { it('uses works with "unix" host', async () => { const workspace = await models.workspace.create(); const settings = await models.settings.create(); - const request = Object.assign(models.request.init(), { _id: 'req_123', parentId: workspace._id, url: 'http://unix:3000/my/path', method: 'GET', }); - const renderedRequest = await getRenderedRequest(request); const response = await networkUtils._actuallySend( renderedRequest, @@ -503,7 +551,6 @@ describe('actuallySend()', () => { workspace, settings, ); - const bodyBuffer = models.response.getBodyBuffer(response); const body = JSON.parse(bodyBuffer); expect(body).toEqual({ @@ -530,7 +577,6 @@ describe('actuallySend()', () => { it('uses netrc', async () => { const workspace = await models.workspace.create(); const settings = await models.settings.create(); - const request = Object.assign(models.request.init(), { _id: 'req_123', parentId: workspace._id, @@ -538,7 +584,6 @@ describe('actuallySend()', () => { type: AUTH_NETRC, }, }); - const renderedRequest = await getRenderedRequest(request); const response = await networkUtils._actuallySend( renderedRequest, @@ -546,7 +591,6 @@ describe('actuallySend()', () => { workspace, settings, ); - const bodyBuffer = models.response.getBodyBuffer(response); const body = JSON.parse(bodyBuffer); expect(body).toEqual({ @@ -574,12 +618,10 @@ describe('actuallySend()', () => { it('sets HTTP version', async () => { const workspace = await models.workspace.create(); const settings = await models.settings.create(); - const request = Object.assign(models.request.init(), { _id: 'req_123', parentId: workspace._id, }); - const renderedRequest = await getRenderedRequest(request); const responseV1 = await networkUtils._actuallySend(renderedRequest, CONTEXT, workspace, { ...settings, @@ -605,7 +647,6 @@ describe('actuallySend()', () => { ...settings, preferredHttpVersion: 'blah', }); - const r = models.response; expect(JSON.parse(r.getBodyBuffer(responseV1)).options.HTTP_VERSION).toBe('V1_0'); expect(JSON.parse(r.getBodyBuffer(responseV11)).options.HTTP_VERSION).toBe('V1_1'); @@ -619,21 +660,18 @@ describe('actuallySend()', () => { // GIVEN const workspace = await models.workspace.create(); const settings = await models.settings.create(); - const request1 = Object.assign(models.request.init(), { _id: 'req_15', parentId: workspace._id, url: 'http://unix:3000/requestA', method: 'GET', }); - const request2 = Object.assign(models.request.init(), { _id: 'req_10', parentId: workspace._id, url: 'http://unix:3000/requestB', method: 'GET', }); - const renderedRequest1 = await getRenderedRequest(request1); const renderedRequest2 = await getRenderedRequest(request2); @@ -644,6 +682,7 @@ describe('actuallySend()', () => { workspace, settings, ); + const response2Promise = networkUtils._actuallySend( renderedRequest2, CONTEXT, @@ -654,11 +693,9 @@ describe('actuallySend()', () => { await networkUtils.cancelRequestById(renderedRequest1._id); const response1 = await response1Promise; const response2 = await response2Promise; - // THEN expect(response1.statusMessage).toBe('Cancelled'); expect(response2.statusMessage).toBe('OK'); - expect(networkUtils.hasCancelFunctionForId(request1._id)).toBe(false); expect(networkUtils.hasCancelFunctionForId(request2._id)).toBe(false); }); @@ -666,6 +703,7 @@ describe('actuallySend()', () => { describe('_getAwsAuthHeaders', () => { beforeEach(globalBeforeEach); + it('should generate expected headers', () => { const req = { authentication: { @@ -674,8 +712,15 @@ describe('_getAwsAuthHeaders', () => { secretAccessKey: 'SAK9999999999999', sessionToken: 'ST9999999999999999', }, - headers: [{ name: 'content-type', value: 'application/json' }], - body: { text: '{}' }, + headers: [ + { + name: 'content-type', + value: 'application/json', + }, + ], + body: { + text: '{}', + }, method: 'POST', url: 'https://ec2.us-west-2.amazonaws.com/path?query=q1', }; @@ -684,6 +729,7 @@ describe('_getAwsAuthHeaders', () => { secretAccessKey: req.authentication.secretAccessKey || '', sessionToken: req.authentication.sessionToken || '', }; + const headers = networkUtils._getAwsAuthHeaders( credentials, req.headers, @@ -691,6 +737,7 @@ describe('_getAwsAuthHeaders', () => { req.url, req.method, ); + expect(filterHeaders(headers, 'x-amz-date')[0].value).toMatch(/^\d{8}T\d{6}Z$/); expect(filterHeaders(headers, 'host')[0].value).toEqual('ec2.us-west-2.amazonaws.com'); expect(filterHeaders(headers, 'authorization')[0].value).toMatch( @@ -716,6 +763,7 @@ describe('_getAwsAuthHeaders', () => { secretAccessKey: req.authentication.secretAccessKey || '', sessionToken: req.authentication.sessionToken || '', }; + const headers = networkUtils._getAwsAuthHeaders( credentials, req.headers, @@ -749,7 +797,6 @@ describe('_parseHeaders', () => { 'Foo', // Invalid header '', ]; - const minimalHeaders = ['HTTP/1.1 301', '']; it('Parses single response headers', () => { @@ -759,15 +806,42 @@ describe('_parseHeaders', () => { version: 'HTTP/1.1', reason: 'Moved Permanently', headers: [ - { name: 'X-Powered-By', value: 'Express' }, - { name: 'location', value: 'http://localhost:3000/' }, - { name: 'Content-Type', value: 'text/plain; charset=utf-8' }, - { name: 'Content-Length', value: '17' }, - { name: 'ETag', value: 'W/"11-WKzg6oYof0o8Mliwrz5pkw"' }, - { name: 'Duplicate', value: 'foo' }, - { name: 'Duplicate', value: 'bar' }, - { name: 'Date', value: 'Mon, 13 Nov 2017 22:06:28 GMT' }, - { name: 'Foo', value: '' }, + { + name: 'X-Powered-By', + value: 'Express', + }, + { + name: 'location', + value: 'http://localhost:3000/', + }, + { + name: 'Content-Type', + value: 'text/plain; charset=utf-8', + }, + { + name: 'Content-Length', + value: '17', + }, + { + name: 'ETag', + value: 'W/"11-WKzg6oYof0o8Mliwrz5pkw"', + }, + { + name: 'Duplicate', + value: 'foo', + }, + { + name: 'Duplicate', + value: 'bar', + }, + { + name: 'Date', + value: 'Mon, 13 Nov 2017 22:06:28 GMT', + }, + { + name: 'Foo', + value: '', + }, ], }, ]); @@ -780,15 +854,42 @@ describe('_parseHeaders', () => { version: 'HTTP/1.1', reason: 'Moved Permanently', headers: [ - { name: 'X-Powered-By', value: 'Express' }, - { name: 'location', value: 'http://localhost:3000/' }, - { name: 'Content-Type', value: 'text/plain; charset=utf-8' }, - { name: 'Content-Length', value: '17' }, - { name: 'ETag', value: 'W/"11-WKzg6oYof0o8Mliwrz5pkw"' }, - { name: 'Duplicate', value: 'foo' }, - { name: 'Duplicate', value: 'bar' }, - { name: 'Date', value: 'Mon, 13 Nov 2017 22:06:28 GMT' }, - { name: 'Foo', value: '' }, + { + name: 'X-Powered-By', + value: 'Express', + }, + { + name: 'location', + value: 'http://localhost:3000/', + }, + { + name: 'Content-Type', + value: 'text/plain; charset=utf-8', + }, + { + name: 'Content-Length', + value: '17', + }, + { + name: 'ETag', + value: 'W/"11-WKzg6oYof0o8Mliwrz5pkw"', + }, + { + name: 'Duplicate', + value: 'foo', + }, + { + name: 'Duplicate', + value: 'bar', + }, + { + name: 'Date', + value: 'Mon, 13 Nov 2017 22:06:28 GMT', + }, + { + name: 'Foo', + value: '', + }, ], }, ]); @@ -802,15 +903,42 @@ describe('_parseHeaders', () => { version: 'HTTP/1.1', reason: 'Moved Permanently', headers: [ - { name: 'X-Powered-By', value: 'Express' }, - { name: 'location', value: 'http://localhost:3000/' }, - { name: 'Content-Type', value: 'text/plain; charset=utf-8' }, - { name: 'Content-Length', value: '17' }, - { name: 'ETag', value: 'W/"11-WKzg6oYof0o8Mliwrz5pkw"' }, - { name: 'Duplicate', value: 'foo' }, - { name: 'Duplicate', value: 'bar' }, - { name: 'Date', value: 'Mon, 13 Nov 2017 22:06:28 GMT' }, - { name: 'Foo', value: '' }, + { + name: 'X-Powered-By', + value: 'Express', + }, + { + name: 'location', + value: 'http://localhost:3000/', + }, + { + name: 'Content-Type', + value: 'text/plain; charset=utf-8', + }, + { + name: 'Content-Length', + value: '17', + }, + { + name: 'ETag', + value: 'W/"11-WKzg6oYof0o8Mliwrz5pkw"', + }, + { + name: 'Duplicate', + value: 'foo', + }, + { + name: 'Duplicate', + value: 'bar', + }, + { + name: 'Date', + value: 'Mon, 13 Nov 2017 22:06:28 GMT', + }, + { + name: 'Foo', + value: '', + }, ], }, { diff --git a/packages/insomnia-app/app/network/__tests__/url-matches-cert-host.test.js b/packages/insomnia-app/app/network/__tests__/url-matches-cert-host.test.ts similarity index 99% rename from packages/insomnia-app/app/network/__tests__/url-matches-cert-host.test.js rename to packages/insomnia-app/app/network/__tests__/url-matches-cert-host.test.ts index 6a2e43c566..ac9f5b3884 100644 --- a/packages/insomnia-app/app/network/__tests__/url-matches-cert-host.test.js +++ b/packages/insomnia-app/app/network/__tests__/url-matches-cert-host.test.ts @@ -5,6 +5,7 @@ describe('urlMatchesCertHost', () => { beforeEach(globalBeforeEach); describe('when the certificate host has no wildcard', () => { beforeEach(globalBeforeEach); + it('should return false if the requested host does not match the certificate host', () => { const requestUrl = 'https://www.example.org'; const certificateHost = 'https://www.example.com'; @@ -56,6 +57,7 @@ describe('urlMatchesCertHost', () => { describe('when using wildcard certificate hosts', () => { beforeEach(globalBeforeEach); + it('should return true if the certificate host is only a wildcard', () => { const requestUrl = 'https://www.example.org/some/resources?query=1'; const certificateHost = '*'; @@ -131,6 +133,7 @@ describe('urlMatchesCertHost', () => { describe('when an invalid certificate host is supplied', () => { beforeEach(globalBeforeEach); + it('should return false if the certificate host contains invalid characters', () => { const requestUrl = 'https://www.example.org/some/resources?query=1'; const certificateHost = 'https://example!.org'; diff --git a/packages/insomnia-app/app/network/authentication.js b/packages/insomnia-app/app/network/authentication.ts similarity index 88% rename from packages/insomnia-app/app/network/authentication.js rename to packages/insomnia-app/app/network/authentication.ts index 10b0bc39bf..f3c79c1eee 100644 --- a/packages/insomnia-app/app/network/authentication.js +++ b/packages/insomnia-app/app/network/authentication.ts @@ -1,4 +1,3 @@ -// @flow import { AUTH_ASAP, AUTH_BASIC, @@ -15,15 +14,12 @@ import type { RenderedRequest } from '../common/render'; import { getBasicAuthHeader } from './basic-auth/get-header'; import { getBearerAuthHeader } from './bearer-auth/get-header'; -type Header = { - name: string, - value: string, -}; +interface Header { + name: string; + value: string; +} -export async function getAuthHeader( - renderedRequest: RenderedRequest, - url: string, -): Promise
{ +export async function getAuthHeader(renderedRequest: RenderedRequest, url: string) { const { method, authentication, body } = renderedRequest; const requestId = renderedRequest._id; @@ -48,8 +44,8 @@ export async function getAuthHeader( // pretending we are fetching a token for the original request. This makes sure // the same tokens are used for schema fetching. See issue #835 on GitHub. const tokenId = requestId.match(/\.graphql$/) ? requestId.replace(/\.graphql$/, '') : requestId; - const oAuth2Token = await getOAuth2Token(tokenId, authentication); + if (oAuth2Token) { const token = oAuth2Token.accessToken; return _buildBearerHeader(token, authentication.tokenPrefix); @@ -60,6 +56,7 @@ export async function getAuthHeader( if (authentication.type === AUTH_OAUTH_1) { const oAuth1Token = await getOAuth1Token(url, method, authentication, body); + if (oAuth1Token) { return { name: 'Authorization', @@ -73,7 +70,11 @@ export async function getAuthHeader( if (authentication.type === AUTH_HAWK) { const { id, key, algorithm, ext, validatePayload } = authentication; let headerOptions = { - credentials: { id, key, algorithm }, + credentials: { + id, + key, + algorithm, + }, ext: ext, }; @@ -88,16 +89,19 @@ export async function getAuthHeader( const header = Hawk.client.header(url, method, headerOptions); return { name: 'Authorization', + // @ts-expect-error -- TSCONVERSION need to update hawk, types only exist for the latest version (v9) and we need those types in order to successfully build value: header.field, }; } if (authentication.type === AUTH_ASAP) { const { issuer, subject, audience, keyId, additionalClaims, privateKey } = authentication; - const generator = jwtAuthentication.client.create(); - let claims = { iss: issuer, sub: subject, aud: audience }; - + let claims = { + iss: issuer, + sub: subject, + aud: audience, + }; let parsedAdditionalClaims; try { @@ -120,8 +124,7 @@ export async function getAuthHeader( privateKey, kid: keyId, }; - - return new Promise((resolve, reject) => { + return new Promise
((resolve, reject) => { generator.generateAuthorizationHeader(claims, options, (error, headerValue) => { if (error) { reject(error); diff --git a/packages/insomnia-app/app/network/axios-request.js b/packages/insomnia-app/app/network/axios-request.ts similarity index 72% rename from packages/insomnia-app/app/network/axios-request.js rename to packages/insomnia-app/app/network/axios-request.ts index 41823c2f56..2b9d3d8cf7 100644 --- a/packages/insomnia-app/app/network/axios-request.js +++ b/packages/insomnia-app/app/network/axios-request.ts @@ -7,28 +7,31 @@ import { isDevelopment } from '../common/constants'; export async function axiosRequest(config) { const settings = await models.settings.getOrCreate(); const isHttps = config.url.indexOf('https:') === 0; + let proxyUrl: string | null = null; - let proxyUrl = null; if (isHttps && settings.httpsProxy) { proxyUrl = settings.httpsProxy; } else if (settings.httpProxy) { proxyUrl = settings.httpProxy; } - const finalConfig = { - ...config, - adapter: global.require('axios/lib/adapters/http'), - }; + const finalConfig = { ...config, adapter: global.require('axios/lib/adapters/http') }; if (proxyUrl) { const { hostname, port } = urlParse(setDefaultProtocol(proxyUrl)); - finalConfig.proxy = { host: hostname, port }; + finalConfig.proxy = { + host: hostname, + port, + }; } const response = await axios(finalConfig); if (isDevelopment()) { - console.log('[axios] Response', { config, response }); + console.log('[axios] Response', { + config, + response, + }); } return response; diff --git a/packages/insomnia-app/app/network/basic-auth/__tests__/get-header.test.js b/packages/insomnia-app/app/network/basic-auth/__tests__/get-header.test.ts similarity index 99% rename from packages/insomnia-app/app/network/basic-auth/__tests__/get-header.test.js rename to packages/insomnia-app/app/network/basic-auth/__tests__/get-header.test.ts index f0f71e2dbe..3cc5678088 100644 --- a/packages/insomnia-app/app/network/basic-auth/__tests__/get-header.test.js +++ b/packages/insomnia-app/app/network/basic-auth/__tests__/get-header.test.ts @@ -3,6 +3,7 @@ import { getBasicAuthHeader } from '../get-header'; describe('getBasicAuthHeader()', () => { beforeEach(globalBeforeEach); + it('succeed with username and password', () => { const header = getBasicAuthHeader('user', 'password'); expect(header).toEqual({ diff --git a/packages/insomnia-app/app/network/basic-auth/get-header.js b/packages/insomnia-app/app/network/basic-auth/get-header.ts similarity index 55% rename from packages/insomnia-app/app/network/basic-auth/get-header.js rename to packages/insomnia-app/app/network/basic-auth/get-header.ts index 2165227573..4439aa96f9 100644 --- a/packages/insomnia-app/app/network/basic-auth/get-header.js +++ b/packages/insomnia-app/app/network/basic-auth/get-header.ts @@ -1,15 +1,18 @@ -// @flow - import type { RequestHeader } from '../../models/request'; export function getBasicAuthHeader( - username: ?string, - password: ?string, - encoding: ?string = 'utf8', -): RequestHeader { + username?: string | null, + password?: string | null, + encoding = 'utf8', +) { const name = 'Authorization'; const header = `${username || ''}:${password || ''}`; + // @ts-expect-error -- TSCONVERSION appears to be a genuine error const authString = Buffer.from(header, encoding).toString('base64'); const value = `Basic ${authString}`; - return { name, value }; + const requestHeader: RequestHeader = { + name, + value, + }; + return requestHeader; } diff --git a/packages/insomnia-app/app/network/bearer-auth/get-header.js b/packages/insomnia-app/app/network/bearer-auth/get-header.ts similarity index 68% rename from packages/insomnia-app/app/network/bearer-auth/get-header.js rename to packages/insomnia-app/app/network/bearer-auth/get-header.ts index 2f111f5da4..a39cb945ca 100644 --- a/packages/insomnia-app/app/network/bearer-auth/get-header.js +++ b/packages/insomnia-app/app/network/bearer-auth/get-header.ts @@ -1,8 +1,11 @@ -// @flow import type { RequestHeader } from '../../models/request'; -export function getBearerAuthHeader(token: string, prefix: string): RequestHeader { +export function getBearerAuthHeader(token: string, prefix: string) { const name = 'Authorization'; const value = `${prefix || 'Bearer'} ${token}`; - return { name, value }; + const requestHeader: RequestHeader = { + name, + value, + }; + return requestHeader; } diff --git a/packages/insomnia-app/app/network/ca-certs.js b/packages/insomnia-app/app/network/ca-certs.js index 8e74ce2489..d9b6197831 100644 --- a/packages/insomnia-app/app/network/ca-certs.js +++ b/packages/insomnia-app/app/network/ca-certs.js @@ -1,6 +1,7 @@ +// NOTE: this file is used by the `val-loader` webpack loader to export the NodeJS trust store during compile time. +// Do not convert it to TypeScript. It's a native node module by design. const tls = require('tls'); -// Used by val-loader to export the NodeJS trust store during compile time module.exports = () => { return { code: 'module.exports = `' + tls.rootCertificates.join('\n') + '`', diff --git a/packages/insomnia-app/app/network/certificate-url-parse.js b/packages/insomnia-app/app/network/certificate-url-parse.ts similarity index 90% rename from packages/insomnia-app/app/network/certificate-url-parse.js rename to packages/insomnia-app/app/network/certificate-url-parse.ts index 4da7584ed7..d102bff2db 100644 --- a/packages/insomnia-app/app/network/certificate-url-parse.js +++ b/packages/insomnia-app/app/network/certificate-url-parse.ts @@ -1,9 +1,7 @@ import { parse as urlParse } from 'url'; const WILDCARD_CHARACTER = '*'; -const WILDCARD_SUBSTITUTION = Math.random() - .toString() - .split('.')[1]; +const WILDCARD_SUBSTITUTION = Math.random().toString().split('.')[1]; const WILDCARD_SUBSTITUTION_PATTERN = new RegExp(`${WILDCARD_SUBSTITUTION}`, 'g'); export default function certificateUrlParse(url) { @@ -15,7 +13,6 @@ export default function certificateUrlParse(url) { parsed.host = _reinstateWildcards(parsed.host); parsed.href = _reinstateWildcards(parsed.href); parsed.port = _reinstateWildcards(parsed.port); - return parsed; } } diff --git a/packages/insomnia-app/app/network/grpc/__mocks__/index.js b/packages/insomnia-app/app/network/grpc/__mocks__/index.js deleted file mode 100644 index 789d67e36a..0000000000 --- a/packages/insomnia-app/app/network/grpc/__mocks__/index.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - start: jest.fn(), - sendMessage: jest.fn(), - commit: jest.fn(), - cancel: jest.fn(), - cancelMultiple: jest.fn(), -}; diff --git a/packages/insomnia-app/app/network/grpc/__mocks__/index.ts b/packages/insomnia-app/app/network/grpc/__mocks__/index.ts new file mode 100644 index 0000000000..2f5f88df4d --- /dev/null +++ b/packages/insomnia-app/app/network/grpc/__mocks__/index.ts @@ -0,0 +1,7 @@ +module.exports = { + start: jest.fn(), + sendMessage: jest.fn(), + commit: jest.fn(), + cancel: jest.fn(), + cancelMultiple: jest.fn(), + }; diff --git a/packages/insomnia-app/app/network/grpc/__schemas__/grpc-ipc-message-params-schema.js b/packages/insomnia-app/app/network/grpc/__schemas__/grpc-ipc-message-params-schema.ts similarity index 84% rename from packages/insomnia-app/app/network/grpc/__schemas__/grpc-ipc-message-params-schema.js rename to packages/insomnia-app/app/network/grpc/__schemas__/grpc-ipc-message-params-schema.ts index 8e98724e30..d3cecb75e2 100644 --- a/packages/insomnia-app/app/network/grpc/__schemas__/grpc-ipc-message-params-schema.js +++ b/packages/insomnia-app/app/network/grpc/__schemas__/grpc-ipc-message-params-schema.ts @@ -1,8 +1,9 @@ -// @flow import type { Schema } from '@develohpanda/fluent-builder'; import type { GrpcIpcMessageParams } from '../prepare'; export const grpcIpcMessageParamsSchema: Schema = { requestId: () => 'gr', - body: () => ({ text: '{}' }), + body: () => ({ + text: '{}', + }), }; diff --git a/packages/insomnia-app/app/network/grpc/__schemas__/grpc-ipc-request-params-schema.js b/packages/insomnia-app/app/network/grpc/__schemas__/grpc-ipc-request-params-schema.ts similarity index 85% rename from packages/insomnia-app/app/network/grpc/__schemas__/grpc-ipc-request-params-schema.js rename to packages/insomnia-app/app/network/grpc/__schemas__/grpc-ipc-request-params-schema.ts index 0d507a61d0..13b35e3046 100644 --- a/packages/insomnia-app/app/network/grpc/__schemas__/grpc-ipc-request-params-schema.js +++ b/packages/insomnia-app/app/network/grpc/__schemas__/grpc-ipc-request-params-schema.ts @@ -1,7 +1,7 @@ -// @flow import type { Schema } from '@develohpanda/fluent-builder'; import type { GrpcIpcRequestParams } from '../prepare'; export const grpcIpcRequestParamsSchema: Schema = { + // @ts-expect-error -- TSCONVERSION request: () => ({}), }; diff --git a/packages/insomnia-app/app/network/grpc/__tests__/call-cache.test.js b/packages/insomnia-app/app/network/grpc/__tests__/call-cache.test.ts similarity index 76% rename from packages/insomnia-app/app/network/grpc/__tests__/call-cache.test.js rename to packages/insomnia-app/app/network/grpc/__tests__/call-cache.test.ts index 4a93972aa8..5f6e5c6bc8 100644 --- a/packages/insomnia-app/app/network/grpc/__tests__/call-cache.test.js +++ b/packages/insomnia-app/app/network/grpc/__tests__/call-cache.test.ts @@ -1,5 +1,3 @@ -// @flow - import callCache from '../call-cache'; describe('call-cache', () => { @@ -8,36 +6,38 @@ describe('call-cache', () => { it('should set, get and update a call in the cache', () => { const id1 = 'abc'; const id2 = 'def'; - const val1 = { a: true }; - const val2 = { b: false }; - + const val1 = { + a: true, + }; + const val2 = { + b: false, + }; callCache.set(id1, val1); callCache.set(id2, val2); - expect(callCache.get(id1)).toBe(val1); expect(callCache.get(id2)).toBe(val2); - - const updatedVal1 = { b: false }; + const updatedVal1 = { + b: false, + }; callCache.set(id1, updatedVal1); expect(callCache.get(id1)).toBe(updatedVal1); }); it('should log to console if id not found', () => { const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); - const call = callCache.get('abc'); - - expect(consoleLogSpy).toHaveBeenCalledWith(`[gRPC] client call for req=abc not found`); + expect(consoleLogSpy).toHaveBeenCalledWith('[gRPC] client call for req=abc not found'); expect(call).toBe(undefined); }); it('should reset', () => { const id1 = 'abc'; const id2 = 'abc'; - const val = { a: true }; + const val = { + a: true, + }; callCache.set(id1, val); callCache.set(id2, val); - expect(callCache.get(id1)).toBe(val); expect(callCache.get(id2)).toBe(val); callCache.reset(); @@ -47,28 +47,31 @@ describe('call-cache', () => { it('should clear and close channel', () => { const id = 'abc'; - const channel = { close: jest.fn() }; - const call = { call: { call: { channel } } }; - + const channel = { + close: jest.fn(), + }; + const call = { + call: { + call: { + channel, + }, + }, + }; callCache.set(id, call); callCache.clear(id); - expect(callCache.get(id)).toBe(undefined); expect(channel.close).toHaveBeenCalledTimes(1); }); it('should clear and log to console if channel not found', () => { const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); - const id = 'abc'; const call = {}; - callCache.set(id, call); callCache.clear(id); - expect(callCache.get(id)).toBe(undefined); expect(consoleLogSpy).toHaveBeenCalledWith( - `[gRPC] failed to close channel for req=abc because it was not found`, + '[gRPC] failed to close channel for req=abc because it was not found', ); }); }); diff --git a/packages/insomnia-app/app/network/grpc/__tests__/index.test.js b/packages/insomnia-app/app/network/grpc/__tests__/index.test.ts similarity index 77% rename from packages/insomnia-app/app/network/grpc/__tests__/index.test.js rename to packages/insomnia-app/app/network/grpc/__tests__/index.test.ts index b278c69fa7..19be9918a0 100644 --- a/packages/insomnia-app/app/network/grpc/__tests__/index.test.js +++ b/packages/insomnia-app/app/network/grpc/__tests__/index.test.ts @@ -1,20 +1,16 @@ -// @flow - import * as grpc from '../index'; import * as protoLoader from '../proto-loader'; import { createBuilder } from '@develohpanda/fluent-builder'; import { grpcIpcRequestParamsSchema } from '../__schemas__/grpc-ipc-request-params-schema'; import { grpcIpcMessageParamsSchema } from '../__schemas__/grpc-ipc-message-params-schema'; - import { ResponseCallbacks as ResponseCallbacksMock } from '../response-callbacks'; import { grpcMethodDefinitionSchema } from '../../../ui/context/grpc/__schemas__'; import { globalBeforeEach } from '../../../__jest__/before-each'; import { grpcMocks } from '../../../__mocks__/@grpc/grpc-js'; import callCache from '../call-cache'; -import { GrpcStatusEnum } from '../service-error'; +import * as grpcJs from '@grpc/grpc-js'; jest.mock('../response-callbacks'); - jest.mock('../proto-loader'); jest.mock('@grpc/grpc-js'); @@ -40,12 +36,15 @@ describe('grpc', () => { it('should exit if method not found', async () => { // Arrange - const params = requestParamsBuilder.request({ _id: 'id', protoMethodName: 'SayHi' }).build(); + const params = requestParamsBuilder + .request({ + _id: 'id', + protoMethodName: 'SayHi', + }) + .build(); protoLoader.getSelectedMethod.mockResolvedValue(null); - // Act await grpc.start(params, respond); - // Assert expect(respond.sendStart).not.toHaveBeenCalled(); expect(respond.sendError).toHaveBeenCalledWith( @@ -56,38 +55,40 @@ describe('grpc', () => { it('should exit if no url is specified', async () => { // Arrange - const params = requestParamsBuilder.request({ _id: 'id', url: '' }).build(); + const params = requestParamsBuilder + .request({ + _id: 'id', + url: '', + }) + .build(); protoLoader.getSelectedMethod.mockResolvedValue(methodBuilder.build()); - // Act await grpc.start(params, respond); - // Assert expect(respond.sendStart).not.toHaveBeenCalled(); expect(respond.sendError).toHaveBeenCalledWith( params.request._id, - new Error(`URL not specified`), + new Error('URL not specified'), ); }); it('should make a client', async () => { // Arrange - const params = requestParamsBuilder.request({ _id: 'id', url: 'grpcb.in:9000' }).build(); - const bidiMethod = methodBuilder - .requestStream(true) - .responseStream(true) + const params = requestParamsBuilder + .request({ + _id: 'id', + url: 'grpcb.in:9000', + }) .build(); + const bidiMethod = methodBuilder.requestStream(true).responseStream(true).build(); protoLoader.getSelectedMethod.mockResolvedValue(bidiMethod); - // Act await grpc.start(params, respond); - // Assert expect(grpcMocks.mockConstructor).toHaveBeenCalledTimes(1); expect(grpcMocks.mockConstructor.mock.calls[0][0]).toBe('grpcb.in:9000'); expect(grpcMocks.mockCreateInsecure).toHaveBeenCalled(); expect(grpcMocks.mockCreateSsl).not.toHaveBeenCalled(); - // Cleanup / End the stream grpcMocks.getMockCall().emit('end'); }); @@ -95,45 +96,44 @@ describe('grpc', () => { it('should make a secure client', async () => { // Arrange const params = requestParamsBuilder - .request({ _id: 'id', url: 'grpcs://grpcb.in:9000' }) - .build(); - const bidiMethod = methodBuilder - .requestStream(true) - .responseStream(true) + .request({ + _id: 'id', + url: 'grpcs://grpcb.in:9000', + }) .build(); + const bidiMethod = methodBuilder.requestStream(true).responseStream(true).build(); protoLoader.getSelectedMethod.mockResolvedValue(bidiMethod); - // Act await grpc.start(params, respond); - // Assert expect(grpcMocks.mockConstructor).toHaveBeenCalledTimes(1); expect(grpcMocks.mockConstructor.mock.calls[0][0]).toBe('grpcb.in:9000'); expect(grpcMocks.mockCreateInsecure).not.toHaveBeenCalled(); expect(grpcMocks.mockCreateSsl).toHaveBeenCalled(); - // Cleanup / End the stream grpcMocks.getMockCall().emit('end'); }); it('should attach status listener', async () => { // Arrange - const params = requestParamsBuilder.request({ _id: 'id', url: 'grpcb.in:9000' }).build(); - const bidiMethod = methodBuilder - .requestStream(true) - .responseStream(true) + const params = requestParamsBuilder + .request({ + _id: 'id', + url: 'grpcb.in:9000', + }) .build(); + const bidiMethod = methodBuilder.requestStream(true).responseStream(true).build(); protoLoader.getSelectedMethod.mockResolvedValue(bidiMethod); - // Act await grpc.start(params, respond); // Emit stats - const status = { code: GrpcStatusEnum.OK, details: 'OK' }; + const status = { + code: grpcJs.status.OK, + details: 'OK', + }; grpcMocks.getMockCall().emit('status', status); - // Assert expect(respond.sendStatus).toHaveBeenCalledWith(params.request._id, status); - // Cleanup / End the stream grpcMocks.getMockCall().emit('end'); }); @@ -142,17 +142,18 @@ describe('grpc', () => { it('should make no request if invalid body', async () => { // Arrange const params = requestParamsBuilder - .request({ _id: 'id', url: 'grpcb.in:9000', body: { text: undefined } }) - .build(); - const unaryMethod = methodBuilder - .requestStream(false) - .responseStream(false) + .request({ + _id: 'id', + url: 'grpcb.in:9000', + body: { + text: undefined, + }, + }) .build(); + const unaryMethod = methodBuilder.requestStream(false).responseStream(false).build(); protoLoader.getSelectedMethod.mockResolvedValue(unaryMethod); - // Act await grpc.start(params, respond); - // Assert expect(respond.sendStart).not.toHaveBeenCalled(); expect(respond.sendError).toHaveBeenCalledWith( @@ -165,17 +166,18 @@ describe('grpc', () => { it('should make unary request with error response', async () => { // Arrange const params = requestParamsBuilder - .request({ _id: 'id', url: 'grpcb.in:9000', body: { text: '{}' } }) - .build(); - const unaryMethod = methodBuilder - .requestStream(false) - .responseStream(false) + .request({ + _id: 'id', + url: 'grpcb.in:9000', + body: { + text: '{}', + }, + }) .build(); + const unaryMethod = methodBuilder.requestStream(false).responseStream(false).build(); protoLoader.getSelectedMethod.mockResolvedValue(unaryMethod); - // Act await grpc.start(params, respond); - // Assert expect(respond.sendStart).toHaveBeenCalledWith(params.request._id); expect(grpcMocks.mockMakeUnaryRequest).toHaveBeenLastCalledWith( @@ -185,12 +187,12 @@ describe('grpc', () => { {}, expect.anything(), ); - // Trigger response - const err = { code: GrpcStatusEnum.DATA_LOSS }; + const err = { + code: grpcJs.status.DATA_LOSS, + }; const val = undefined; grpcMocks.mockMakeUnaryRequest.mock.calls[0][4](err, val); - // Assert expect(respond.sendData).not.toHaveBeenCalled(); expect(respond.sendError).toHaveBeenCalledWith(params.request._id, err); @@ -200,17 +202,18 @@ describe('grpc', () => { it('should make unary request with valid response', async () => { // Arrange const params = requestParamsBuilder - .request({ _id: 'id', url: 'grpcb.in:9000', body: { text: '{}' } }) - .build(); - const unaryMethod = methodBuilder - .requestStream(false) - .responseStream(false) + .request({ + _id: 'id', + url: 'grpcb.in:9000', + body: { + text: '{}', + }, + }) .build(); + const unaryMethod = methodBuilder.requestStream(false).responseStream(false).build(); protoLoader.getSelectedMethod.mockResolvedValue(unaryMethod); - // Act await grpc.start(params, respond); - // Assert expect(respond.sendStart).toHaveBeenCalledWith(params.request._id); expect(grpcMocks.mockMakeUnaryRequest).toHaveBeenLastCalledWith( @@ -220,12 +223,12 @@ describe('grpc', () => { {}, expect.anything(), ); - // Trigger response const err = undefined; - const val = { foo: 'bar' }; + const val = { + foo: 'bar', + }; grpcMocks.mockMakeUnaryRequest.mock.calls[0][4](err, val); - // Assert expect(respond.sendError).not.toHaveBeenCalled(); expect(respond.sendData).toHaveBeenCalledWith(params.request._id, val); @@ -237,17 +240,18 @@ describe('grpc', () => { it('should make no request if invalid body', async () => { // Arrange const params = requestParamsBuilder - .request({ _id: 'id', url: 'grpcb.in:9000', body: { text: undefined } }) - .build(); - const serverMethod = methodBuilder - .requestStream(false) - .responseStream(true) + .request({ + _id: 'id', + url: 'grpcb.in:9000', + body: { + text: undefined, + }, + }) .build(); + const serverMethod = methodBuilder.requestStream(false).responseStream(true).build(); protoLoader.getSelectedMethod.mockResolvedValue(serverMethod); - // Act await grpc.start(params, respond); - // Assert expect(respond.sendStart).not.toHaveBeenCalled(); expect(respond.sendError).toHaveBeenCalledWith( @@ -260,17 +264,18 @@ describe('grpc', () => { it('should make server streaming request with valid and error response', async () => { // Arrange const params = requestParamsBuilder - .request({ _id: 'id', url: 'grpcb.in:9000', body: { text: '{}' } }) - .build(); - const serverMethod = methodBuilder - .requestStream(false) - .responseStream(true) + .request({ + _id: 'id', + url: 'grpcb.in:9000', + body: { + text: '{}', + }, + }) .build(); + const serverMethod = methodBuilder.requestStream(false).responseStream(true).build(); protoLoader.getSelectedMethod.mockResolvedValue(serverMethod); - // Act await grpc.start(params, respond); - // Assert expect(respond.sendStart).toHaveBeenCalledWith(params.request._id); expect(grpcMocks.mockMakeServerStreamRequest).toHaveBeenLastCalledWith( @@ -279,17 +284,18 @@ describe('grpc', () => { serverMethod.responseDeserialize, {}, ); - // Trigger valid response - const val = { foo: 'bar' }; + const val = { + foo: 'bar', + }; grpcMocks.getMockCall().emit('data', val); grpcMocks.getMockCall().emit('data', val); - // Trigger error response - const err = { code: GrpcStatusEnum.DATA_LOSS }; + const err = { + code: grpcJs.status.DATA_LOSS, + }; grpcMocks.getMockCall().emit('error', err); grpcMocks.getMockCall().emit('end'); - // Assert expect(respond.sendData).toHaveBeenNthCalledWith(1, params.request._id, val); expect(respond.sendData).toHaveBeenNthCalledWith(2, params.request._id, val); @@ -301,16 +307,16 @@ describe('grpc', () => { describe('client streaming', () => { it('should make client streaming request with error response', async () => { // Arrange - const params = requestParamsBuilder.request({ _id: 'id', url: 'grpcb.in:9000' }).build(); - const clientMethod = methodBuilder - .requestStream(true) - .responseStream(false) + const params = requestParamsBuilder + .request({ + _id: 'id', + url: 'grpcb.in:9000', + }) .build(); + const clientMethod = methodBuilder.requestStream(true).responseStream(false).build(); protoLoader.getSelectedMethod.mockResolvedValue(clientMethod); - // Act await grpc.start(params, respond); - // Assert expect(respond.sendStart).toHaveBeenCalledWith(params.request._id); expect(grpcMocks.mockMakeClientStreamRequest).toHaveBeenLastCalledWith( @@ -319,12 +325,12 @@ describe('grpc', () => { clientMethod.responseDeserialize, expect.anything(), ); - // Trigger response - const err = { code: GrpcStatusEnum.DATA_LOSS }; + const err = { + code: grpcJs.status.DATA_LOSS, + }; const val = undefined; grpcMocks.mockMakeClientStreamRequest.mock.calls[0][3](err, val); - // Assert expect(respond.sendData).not.toHaveBeenCalled(); expect(respond.sendError).toHaveBeenCalledWith(params.request._id, err); @@ -333,16 +339,16 @@ describe('grpc', () => { it('should make client streaming request with valid response', async () => { // Arrange - const params = requestParamsBuilder.request({ _id: 'id', url: 'grpcb.in:9000' }).build(); - const clientMethod = methodBuilder - .requestStream(true) - .responseStream(false) + const params = requestParamsBuilder + .request({ + _id: 'id', + url: 'grpcb.in:9000', + }) .build(); + const clientMethod = methodBuilder.requestStream(true).responseStream(false).build(); protoLoader.getSelectedMethod.mockResolvedValue(clientMethod); - // Act await grpc.start(params, respond); - // Assert expect(respond.sendStart).toHaveBeenCalledWith(params.request._id); expect(grpcMocks.mockMakeClientStreamRequest).toHaveBeenLastCalledWith( @@ -351,12 +357,12 @@ describe('grpc', () => { clientMethod.responseDeserialize, expect.anything(), ); - // Trigger response const err = undefined; - const val = { foo: 'bar' }; + const val = { + foo: 'bar', + }; grpcMocks.mockMakeClientStreamRequest.mock.calls[0][3](err, val); - // Assert expect(respond.sendError).not.toHaveBeenCalled(); expect(respond.sendData).toHaveBeenCalledWith(params.request._id, val); @@ -367,16 +373,16 @@ describe('grpc', () => { describe('bidi streaming', () => { it('should make bidi streaming request with valid and error response', async () => { // Arrange - const params = requestParamsBuilder.request({ _id: 'id', url: 'grpcb.in:9000' }).build(); - const bidiMethod = methodBuilder - .requestStream(true) - .responseStream(true) + const params = requestParamsBuilder + .request({ + _id: 'id', + url: 'grpcb.in:9000', + }) .build(); + const bidiMethod = methodBuilder.requestStream(true).responseStream(true).build(); protoLoader.getSelectedMethod.mockResolvedValue(bidiMethod); - // Act await grpc.start(params, respond); - // Assert expect(respond.sendStart).toHaveBeenCalledWith(params.request._id); expect(grpcMocks.mockMakeBidiStreamRequest).toHaveBeenLastCalledWith( @@ -384,17 +390,18 @@ describe('grpc', () => { bidiMethod.requestSerialize, bidiMethod.responseDeserialize, ); - // Trigger valid response - const val = { foo: 'bar' }; + const val = { + foo: 'bar', + }; grpcMocks.getMockCall().emit('data', val); grpcMocks.getMockCall().emit('data', val); - // Trigger error response - const err = { code: GrpcStatusEnum.DATA_LOSS }; + const err = { + code: grpcJs.status.DATA_LOSS, + }; grpcMocks.getMockCall().emit('error', err); grpcMocks.getMockCall().emit('end'); - // Assert expect(respond.sendData).toHaveBeenNthCalledWith(1, params.request._id, val); expect(respond.sendData).toHaveBeenNthCalledWith(2, params.request._id, val); @@ -406,11 +413,13 @@ describe('grpc', () => { describe('grpc.sendMessage', () => { const _makeClient = async () => { - const params = requestParamsBuilder.request({ _id: 'id', url: 'grpcb.in:9000' }).build(); - const clientMethod = methodBuilder - .requestStream(true) - .responseStream(false) + const params = requestParamsBuilder + .request({ + _id: 'id', + url: 'grpcb.in:9000', + }) .build(); + const clientMethod = methodBuilder.requestStream(true).responseStream(false).build(); protoLoader.getSelectedMethod.mockResolvedValue(clientMethod); await grpc.start(params, respond); return params; @@ -420,13 +429,13 @@ describe('grpc', () => { // Arrange const reqParams = await _makeClient(); const msgParams = messageParamsBuilder - .body({ text: undefined }) + .body({ + text: undefined, + }) .requestId(reqParams.request._id) .build(); - // Act grpc.sendMessage(msgParams, respond); - // Assert expect(respond.sendError).toHaveBeenCalledWith( msgParams.requestId, @@ -438,10 +447,8 @@ describe('grpc', () => { // Arrange const reqParams = await _makeClient(); const msgParams = messageParamsBuilder.requestId(reqParams.request._id).build(); - // Act grpc.sendMessage(msgParams, respond); - setTimeout(() => { // Assert expect(respond.sendError).not.toHaveBeenCalled(); @@ -453,12 +460,9 @@ describe('grpc', () => { // Arrange const msgParams = messageParamsBuilder.build(); const mockWrite = jest.fn(); - grpcMocks.getMockCall().on('write', mockWrite); - // Act grpc.sendMessage(msgParams, respond); - // Assert setTimeout(() => { // Assert @@ -470,11 +474,13 @@ describe('grpc', () => { describe('grpc.commit', () => { const _makeClient = async () => { - const params = requestParamsBuilder.request({ _id: 'id', url: 'grpcb.in:9000' }).build(); - const clientMethod = methodBuilder - .requestStream(true) - .responseStream(false) + const params = requestParamsBuilder + .request({ + _id: 'id', + url: 'grpcb.in:9000', + }) .build(); + const clientMethod = methodBuilder.requestStream(true).responseStream(false).build(); protoLoader.getSelectedMethod.mockResolvedValue(clientMethod); await grpc.start(params, respond); return params; @@ -483,10 +489,8 @@ describe('grpc', () => { it('should commit', async () => { // Arrange const reqParams = await _makeClient(); - // Act grpc.commit(reqParams.request._id); - // Assert expect(grpcMocks.mockCallEnd).toHaveBeenCalled(); }); @@ -494,7 +498,6 @@ describe('grpc', () => { it('should not commit if a call is not found', () => { // Act grpc.commit('another id'); - // Assert expect(grpcMocks.mockCallEnd).not.toHaveBeenCalled(); }); @@ -502,11 +505,13 @@ describe('grpc', () => { describe('grpc.cancel', () => { const _makeClient = async () => { - const params = requestParamsBuilder.request({ _id: 'id', url: 'grpcb.in:9000' }).build(); - const clientMethod = methodBuilder - .requestStream(true) - .responseStream(false) + const params = requestParamsBuilder + .request({ + _id: 'id', + url: 'grpcb.in:9000', + }) .build(); + const clientMethod = methodBuilder.requestStream(true).responseStream(false).build(); protoLoader.getSelectedMethod.mockResolvedValue(clientMethod); await grpc.start(params, respond); return params; @@ -515,10 +520,8 @@ describe('grpc', () => { it('should commit', async () => { // Arrange const reqParams = await _makeClient(); - // Act grpc.cancel(reqParams.request._id); - // Assert expect(grpcMocks.mockCallCancel).toHaveBeenCalled(); }); @@ -526,7 +529,6 @@ describe('grpc', () => { it('should not commit if a call is not found', () => { // Act grpc.cancel('another id'); - // Assert expect(grpcMocks.mockCallCancel).not.toHaveBeenCalled(); }); diff --git a/packages/insomnia-app/app/network/grpc/__tests__/method.test.js b/packages/insomnia-app/app/network/grpc/__tests__/method.test.ts similarity index 68% rename from packages/insomnia-app/app/network/grpc/__tests__/method.test.js rename to packages/insomnia-app/app/network/grpc/__tests__/method.test.ts index c8888f389c..3a242b44e4 100644 --- a/packages/insomnia-app/app/network/grpc/__tests__/method.test.js +++ b/packages/insomnia-app/app/network/grpc/__tests__/method.test.ts @@ -1,31 +1,41 @@ -// @flow - import { canClientStream, getMethodType, GrpcMethodTypeEnum, GrpcMethodTypeName } from '../method'; import type { GrpcMethodType } from '../method'; describe('getMethodType', () => { it('should return unary', () => { - expect(getMethodType({ requestStream: false, responseStream: false })).toBe( - GrpcMethodTypeEnum.unary, - ); + expect( + getMethodType({ + requestStream: false, + responseStream: false, + }), + ).toBe(GrpcMethodTypeEnum.unary); }); it('should return server', () => { - expect(getMethodType({ requestStream: false, responseStream: true })).toBe( - GrpcMethodTypeEnum.server, - ); + expect( + getMethodType({ + requestStream: false, + responseStream: true, + }), + ).toBe(GrpcMethodTypeEnum.server); }); it('should return client', () => { - expect(getMethodType({ requestStream: true, responseStream: false })).toBe( - GrpcMethodTypeEnum.client, - ); + expect( + getMethodType({ + requestStream: true, + responseStream: false, + }), + ).toBe(GrpcMethodTypeEnum.client); }); it('should return bidi', () => { - expect(getMethodType({ requestStream: true, responseStream: true })).toBe( - GrpcMethodTypeEnum.bidi, - ); + expect( + getMethodType({ + requestStream: true, + responseStream: true, + }), + ).toBe(GrpcMethodTypeEnum.bidi); }); }); diff --git a/packages/insomnia-app/app/network/grpc/__tests__/parse-grpc-url.test.js b/packages/insomnia-app/app/network/grpc/__tests__/parse-grpc-url.test.ts similarity index 100% rename from packages/insomnia-app/app/network/grpc/__tests__/parse-grpc-url.test.js rename to packages/insomnia-app/app/network/grpc/__tests__/parse-grpc-url.test.ts diff --git a/packages/insomnia-app/app/network/grpc/__tests__/prepare.test.js b/packages/insomnia-app/app/network/grpc/__tests__/prepare.test.ts similarity index 72% rename from packages/insomnia-app/app/network/grpc/__tests__/prepare.test.js rename to packages/insomnia-app/app/network/grpc/__tests__/prepare.test.ts index 87fbb4f4cc..cd91c5fd72 100644 --- a/packages/insomnia-app/app/network/grpc/__tests__/prepare.test.js +++ b/packages/insomnia-app/app/network/grpc/__tests__/prepare.test.ts @@ -17,12 +17,14 @@ describe('prepareGrpcRequest', () => { 'should prepare grpc request with all properties: %s', async methodType => { const w = await models.workspace.create(); - const env = await models.environment.create({ parentId: w._id }); - const gr = await models.grpcRequest.create({ parentId: w._id }); + const env = await models.environment.create({ + parentId: w._id, + }); + const gr = await models.grpcRequest.create({ + parentId: w._id, + }); getRenderedGrpcRequest.mockResolvedValue(gr); - const result = await prepareGrpcRequest(gr._id, env._id, methodType); - expect(getRenderedGrpcRequest).toHaveBeenLastCalledWith( gr, env, @@ -30,8 +32,9 @@ describe('prepareGrpcRequest', () => { {}, false, ); - - expect(result).toEqual({ request: gr }); + expect(result).toEqual({ + request: gr, + }); }, ); @@ -39,12 +42,14 @@ describe('prepareGrpcRequest', () => { 'should prepare grpc request and ignore body: %s', async methodType => { const w = await models.workspace.create(); - const env = await models.environment.create({ parentId: w._id }); - const gr = await models.grpcRequest.create({ parentId: w._id }); + const env = await models.environment.create({ + parentId: w._id, + }); + const gr = await models.grpcRequest.create({ + parentId: w._id, + }); getRenderedGrpcRequest.mockResolvedValue(gr); - const result = await prepareGrpcRequest(gr._id, env._id, methodType); - expect(getRenderedGrpcRequest).toHaveBeenLastCalledWith( gr, env, @@ -52,8 +57,9 @@ describe('prepareGrpcRequest', () => { {}, true, ); - - expect(result).toEqual({ request: gr }); + expect(result).toEqual({ + request: gr, + }); }, ); }); @@ -63,20 +69,23 @@ describe('prepareGrpcMessage', () => { it('should prepare grpc message with only body', async () => { const w = await models.workspace.create(); - const env = await models.environment.create({ parentId: w._id }); - const gr = await models.grpcRequest.create({ parentId: w._id }); - + const env = await models.environment.create({ + parentId: w._id, + }); + const gr = await models.grpcRequest.create({ + parentId: w._id, + }); getRenderedGrpcRequestMessage.mockResolvedValue(gr.body); - const result = await prepareGrpcMessage(gr._id, env._id); - expect(getRenderedGrpcRequestMessage).toHaveBeenLastCalledWith( gr, env, RENDER_PURPOSE_SEND, {}, ); - - expect(result).toEqual({ body: gr.body, requestId: gr._id }); + expect(result).toEqual({ + body: gr.body, + requestId: gr._id, + }); }); }); diff --git a/packages/insomnia-app/app/network/grpc/__tests__/proto-integration.test.js b/packages/insomnia-app/app/network/grpc/__tests__/proto-integration.test.ts similarity index 92% rename from packages/insomnia-app/app/network/grpc/__tests__/proto-integration.test.js rename to packages/insomnia-app/app/network/grpc/__tests__/proto-integration.test.ts index a84284a4d1..197845daa0 100644 --- a/packages/insomnia-app/app/network/grpc/__tests__/proto-integration.test.js +++ b/packages/insomnia-app/app/network/grpc/__tests__/proto-integration.test.ts @@ -1,4 +1,3 @@ -// @flow import path from 'path'; import fs from 'fs'; import os from 'os'; @@ -18,76 +17,67 @@ describe('proto management integration test', () => { it('can ingest proto file and load methods from it', async () => { const w = await models.workspace.create(); - // Mock folder selection const protoFilePath = path.join(__dirname, '../__fixtures__/library/hello.proto'); - selectFileOrFolder.mockResolvedValue({ filePath: protoFilePath }); - + selectFileOrFolder.mockResolvedValue({ + filePath: protoFilePath, + }); // Ingest into database let createdProtoFileId; await protoManager.addFile(w._id, id => { createdProtoFileId = id; }); - expect(selectFileOrFolder).toHaveBeenCalledWith({ itemTypes: ['file'], extensions: ['proto'], }); - // Find proto file entries const helloProto = await models.protoFile.getById(createdProtoFileId); - // Load protoMethods const helloMethods = await protoLoader.loadMethods(helloProto); - expect(helloMethods.length).toBe(4); }); it('can ingest proto directory tree and load methods from any file', async () => { const w = await models.workspace.create(); - // Mock folder selection const libraryDirPath = path.join(__dirname, '../__fixtures__/library'); - selectFileOrFolder.mockResolvedValue({ filePath: libraryDirPath }); - + selectFileOrFolder.mockResolvedValue({ + filePath: libraryDirPath, + }); // Ingest into database await protoManager.addDirectory(w._id); - expect(selectFileOrFolder).toHaveBeenCalledWith({ itemTypes: ['directory'], extensions: ['proto'], }); - // Find proto file entries const protoFiles = await models.protoFile.all(); const rootProto = protoFiles.find(pf => pf.name === 'root.proto'); const helloProto = protoFiles.find(pf => pf.name === 'hello.proto'); const timeProto = protoFiles.find(pf => pf.name === 'time.proto'); - // Load protoMethods const rootMethods = await protoLoader.loadMethods(rootProto); const helloMethods = await protoLoader.loadMethods(helloProto); const timeMethods = await protoLoader.loadMethods(timeProto); - expect(rootMethods.length).toBe(helloMethods.length + timeMethods.length); expect(helloMethods.length).toBe(4); expect(timeMethods.length).toBe(1); - // Create request const gr = await models.grpcRequest.create({ parentId: w._id, protoFileId: rootProto._id, protoMethodName: rootMethods[0].path, }); - // Load selected method const selectedMethod = await protoLoader.getSelectedMethod(gr); - expect(selectedMethod.originalName).toEqual(rootMethods[0].originalName); }); afterEach(async () => { const tempDirPath = path.join(os.tmpdir(), 'insomnia-grpc'); - await fs.promises.rmdir(tempDirPath, { recursive: true }); + await fs.promises.rmdir(tempDirPath, { + recursive: true, + }); }); }); diff --git a/packages/insomnia-app/app/network/grpc/__tests__/response-callbacks.test.js b/packages/insomnia-app/app/network/grpc/__tests__/response-callbacks.test.ts similarity index 94% rename from packages/insomnia-app/app/network/grpc/__tests__/response-callbacks.test.js rename to packages/insomnia-app/app/network/grpc/__tests__/response-callbacks.test.ts index 36f4aaabb5..5fb99f8d4a 100644 --- a/packages/insomnia-app/app/network/grpc/__tests__/response-callbacks.test.js +++ b/packages/insomnia-app/app/network/grpc/__tests__/response-callbacks.test.ts @@ -1,52 +1,46 @@ -// @flow import { ResponseCallbacks } from '../response-callbacks'; import { GrpcResponseEventEnum } from '../../../common/grpc-events'; describe('response-callbacks', () => { - const event = { reply: jest.fn() }; + const event = { + reply: jest.fn(), + }; const id = 'abc'; - beforeEach(() => { jest.resetAllMocks(); }); it('should sendData with expected arguments', () => { - const val = { a: 'b' }; - + const val = { + a: 'b', + }; new ResponseCallbacks(event).sendData(id, val); - expect(event.reply).toHaveBeenCalledTimes(1); expect(event.reply).toHaveBeenCalledWith(GrpcResponseEventEnum.data, id, val); }); it('should sendError with expected arguments', () => { const err = new Error('this is an error'); - new ResponseCallbacks(event).sendError(id, err); - expect(event.reply).toHaveBeenCalledTimes(1); expect(event.reply).toHaveBeenCalledWith(GrpcResponseEventEnum.error, id, err); }); it('should sendEnd with expected arguments', () => { new ResponseCallbacks(event).sendEnd(id); - expect(event.reply).toHaveBeenCalledTimes(1); expect(event.reply).toHaveBeenCalledWith(GrpcResponseEventEnum.end, id); }); it('should sendStart with expected arguments', () => { new ResponseCallbacks(event).sendStart(id); - expect(event.reply).toHaveBeenCalledTimes(1); expect(event.reply).toHaveBeenCalledWith(GrpcResponseEventEnum.start, id); }); it('should sendStatus with expected arguments', () => { const obj = {}; - new ResponseCallbacks(event).sendStatus(id, obj); - expect(event.reply).toHaveBeenCalledTimes(1); expect(event.reply).toHaveBeenCalledWith(GrpcResponseEventEnum.status, id, obj); }); diff --git a/packages/insomnia-app/app/network/grpc/call-cache.js b/packages/insomnia-app/app/network/grpc/call-cache.ts similarity index 66% rename from packages/insomnia-app/app/network/grpc/call-cache.js rename to packages/insomnia-app/app/network/grpc/call-cache.ts index 55e5058d6e..742e3b89a7 100644 --- a/packages/insomnia-app/app/network/grpc/call-cache.js +++ b/packages/insomnia-app/app/network/grpc/call-cache.ts @@ -1,12 +1,7 @@ -// @flow +import { Call } from '@grpc/grpc-js'; -// The types of a call are defined in packages/insomnia-app/node_modules/@grpc/grpc-js/src/call.ts -// These are TS types and too complex to translate entirely to flow types -// A call can be a ClientUnaryCall, ClientReadableStream, ClientWritableStream, or ClientDuplexStream // A call can also emit 'metadata' and 'status' events -export type Call = Object; - -let _calls: { [requestId: string]: Call } = {}; +let _calls: Record = {}; const activeCount = () => Object.keys(_calls).length; @@ -25,7 +20,9 @@ const set = (requestId: string, call: Call): void => { }; const _tryCloseChannel = (requestId: string) => { + // @ts-expect-error -- TSCONVERSION channel not found in call const channel = get(requestId)?.call?.call.channel; + if (channel) { channel.close(); } else { @@ -35,6 +32,7 @@ const _tryCloseChannel = (requestId: string) => { const clear = (requestId: string): void => { _tryCloseChannel(requestId); + delete _calls[requestId]; }; @@ -42,6 +40,11 @@ const reset = (): void => { _calls = {}; }; -const callCache = { activeCount, get, set, clear, reset }; - +const callCache = { + activeCount, + get, + set, + clear, + reset, +}; export default callCache; diff --git a/packages/insomnia-app/app/network/grpc/index.js b/packages/insomnia-app/app/network/grpc/index.ts similarity index 86% rename from packages/insomnia-app/app/network/grpc/index.js rename to packages/insomnia-app/app/network/grpc/index.ts index d337da3843..5020373a80 100644 --- a/packages/insomnia-app/app/network/grpc/index.js +++ b/packages/insomnia-app/app/network/grpc/index.ts @@ -1,13 +1,7 @@ -// @flow - import * as grpc from '@grpc/grpc-js'; - import * as models from '../../models'; import * as protoLoader from './proto-loader'; import callCache from './call-cache'; -import type { ServiceError } from './service-error'; -import { GrpcStatusEnum } from './service-error'; -import type { Call } from './call-cache'; import parseGrpcUrl from './parse-grpc-url'; import type { GrpcIpcMessageParams, GrpcIpcRequestParams } from './prepare'; import { ResponseCallbacks } from './response-callbacks'; @@ -15,8 +9,13 @@ import { getMethodType, GrpcMethodTypeEnum } from './method'; import type { GrpcRequest } from '../../models/grpc-request'; import type { GrpcMethodDefinition } from './method'; import { trackSegmentEvent } from '../../common/analytics'; +import { ServiceClient } from '@grpc/grpc-js/build/src/make-client'; +import { Call, ServiceError } from '@grpc/grpc-js'; -const _createClient = (req: GrpcRequest, respond: ResponseCallbacks): Object | undefined => { +const _createClient = ( + req: GrpcRequest, + respond: ResponseCallbacks, +): ServiceClient | undefined => { const { url, enableTls } = parseGrpcUrl(req.url); if (!url) { @@ -25,8 +24,8 @@ const _createClient = (req: GrpcRequest, respond: ResponseCallbacks): Object | u } const credentials = enableTls ? grpc.credentials.createSsl() : grpc.credentials.createInsecure(); - console.log(`[gRPC] connecting to url=${url} ${enableTls ? 'with' : 'without'} TLS`); + // @ts-expect-error -- TSCONVERSION second argument should be provided, send an empty string? Needs testing const Client = grpc.makeGenericClientConstructor({}); return new Client(url, credentials); }; @@ -45,6 +44,7 @@ const _makeUnaryRequest = ( // Load initial message const messageBody = _parseMessage(bodyText, requestId, respond); + if (!messageBody) { return; } @@ -83,6 +83,7 @@ const _makeServerStreamRequest = ( ): Call | undefined => { // Load initial message const messageBody = _parseMessage(bodyText, requestId, respond); + if (!messageBody) { return; } @@ -114,17 +115,17 @@ const _makeBidiStreamRequest = ({ return call; }; -type RequestData = { - requestId: string, - respond: ResponseCallbacks, - client: Object, - method: GrpcMethodDefinition, -}; +interface RequestData { + requestId: string; + respond: ResponseCallbacks; + client: ServiceClient; + method: GrpcMethodDefinition; +} export const start = async ( { request }: GrpcIpcRequestParams, respond: ResponseCallbacks, -): Promise => { +) => { const requestId = request._id; const method = await protoLoader.getSelectedMethod(request); @@ -140,13 +141,19 @@ export const start = async ( // Create client const client = _createClient(request, respond); + if (!client) { return; } - const requestParams: RequestData = { requestId, client, method, respond }; - + const requestParams: RequestData = { + requestId, + client, + method, + respond, + }; let call; + switch (methodType) { case GrpcMethodTypeEnum.unary: call = _makeUnaryRequest(requestParams, request.body.text || ''); @@ -174,8 +181,8 @@ export const start = async ( trackSegmentEvent('Request Executed'); _setupStatusListener(call, requestId, respond); - respond.sendStart(requestId); + respond.sendStart(requestId); // Save call callCache.set(requestId, call); }; @@ -185,6 +192,7 @@ export const sendMessage = ( respond: ResponseCallbacks, ) => { const messageBody = _parseMessage(body.text || '', requestId, respond); + if (!messageBody) { return; } @@ -193,14 +201,14 @@ export const sendMessage = ( // this must happen in the next tick otherwise the stream does not flush correctly // Try removing it and using a bidi RPC and notice messages don't send consistently process.nextTick(() => { + // @ts-expect-error -- TSCONVERSION only write if the call is ClientWritableStream | ClientDuplexStream callCache.get(requestId)?.write(messageBody, _streamWriteCallback); }); }; +// @ts-expect-error -- TSCONVERSION only end if the call is ClientWritableStream | ClientDuplexStream export const commit = (requestId: string) => callCache.get(requestId)?.end(); - export const cancel = (requestId: string) => callCache.get(requestId)?.cancel(); - export const cancelMultiple = (requestIds: Array) => requestIds.forEach(cancel); const _setupStatusListener = (call: Call, requestId: string, respond: ResponseCallbacks) => { @@ -209,13 +217,12 @@ const _setupStatusListener = (call: Call, requestId: string, respond: ResponseCa const _setupServerStreamListeners = (call: Call, requestId: string, respond: ResponseCallbacks) => { call.on('data', data => respond.sendData(requestId, data)); - call.on('error', (error: ServiceError) => { - if (error && error.code !== GrpcStatusEnum.CANCELLED) { + if (error && error.code !== grpc.status.CANCELLED) { respond.sendError(requestId, error); // Taken through inspiration from other implementation, needs validation - if (error.code === GrpcStatusEnum.UNKNOWN || error.code === GrpcStatusEnum.UNAVAILABLE) { + if (error.code === grpc.status.UNKNOWN || error.code === grpc.status.UNAVAILABLE) { respond.sendEnd(requestId); callCache.clear(requestId); } @@ -230,17 +237,18 @@ const _setupServerStreamListeners = (call: Call, requestId: string, respond: Res // This function returns a function const _createUnaryCallback = (requestId: string, respond: ResponseCallbacks) => ( err: ServiceError, - value: Object, + value: Record, ) => { if (err) { // Don't do anything if cancelled // TODO: test with other errors - if (err.code !== GrpcStatusEnum.CANCELLED) { + if (err.code !== grpc.status.CANCELLED) { respond.sendError(requestId, err); } } else { respond.sendData(requestId, value); } + respond.sendEnd(requestId); callCache.clear(requestId); }; @@ -257,7 +265,7 @@ const _parseMessage = ( bodyText: string, requestId: string, respond: ResponseCallbacks, -): Object | undefined => { +): Record | undefined => { try { return JSON.parse(bodyText); } catch (e) { diff --git a/packages/insomnia-app/app/network/grpc/method.js b/packages/insomnia-app/app/network/grpc/method.ts similarity index 67% rename from packages/insomnia-app/app/network/grpc/method.js rename to packages/insomnia-app/app/network/grpc/method.ts index ea0abbad55..9be8bd7ef7 100644 --- a/packages/insomnia-app/app/network/grpc/method.js +++ b/packages/insomnia-app/app/network/grpc/method.ts @@ -1,23 +1,17 @@ -// @flow +import { MethodDefinition } from '@grpc/grpc-js'; +import { ValueOf } from 'type-fest'; -// https://grpc.github.io/grpc/node/grpc.html#~MethodDefinition -export type GrpcMethodDefinition = { - path: string, - originalName: string, - requestStream: boolean, - responseStream: boolean, - requestSerialize: Function, - responseDeserialize: Function, -}; +// TODO(TSCONVERSION) remove this alias and type MethodDefinition correctly +export type GrpcMethodDefinition = MethodDefinition export const GrpcMethodTypeEnum = { unary: 'unary', server: 'server', client: 'client', bidi: 'bidi', -}; +} as const; -export type GrpcMethodType = $Values; +export type GrpcMethodType = ValueOf; export const getMethodType = ({ requestStream, @@ -38,19 +32,19 @@ export const getMethodType = ({ } }; -export const GrpcMethodTypeName: { [GrpcMethodType]: string } = { +export const GrpcMethodTypeName = { [GrpcMethodTypeEnum.unary]: 'Unary', [GrpcMethodTypeEnum.server]: 'Server Streaming', [GrpcMethodTypeEnum.client]: 'Client Streaming', [GrpcMethodTypeEnum.bidi]: 'Bi-directional Streaming', -}; +} as const; -export const GrpcMethodTypeAcronym: { [GrpcMethodType]: string } = { +export const GrpcMethodTypeAcronym = { [GrpcMethodTypeEnum.unary]: 'U', [GrpcMethodTypeEnum.server]: 'SS', [GrpcMethodTypeEnum.client]: 'CS', [GrpcMethodTypeEnum.bidi]: 'BD', -}; +} as const; export const canClientStream = (methodType?: GrpcMethodType) => methodType === GrpcMethodTypeEnum.client || methodType === GrpcMethodTypeEnum.bidi; diff --git a/packages/insomnia-app/app/network/grpc/parse-grpc-url.js b/packages/insomnia-app/app/network/grpc/parse-grpc-url.js deleted file mode 100644 index 2e363bee25..0000000000 --- a/packages/insomnia-app/app/network/grpc/parse-grpc-url.js +++ /dev/null @@ -1,17 +0,0 @@ -// @flow -import url from 'url'; - -const parseGrpcUrl = (grpcUrl?: string): { url: string, enableTls: boolean } => { - const { protocol, host, href } = url.parse(grpcUrl?.toLowerCase() || ''); - - switch (protocol) { - case 'grpcs:': - return { url: host, enableTls: true }; - case 'grpc:': - return { url: host, enableTls: false }; - default: - return { url: href, enableTls: false }; - } -}; - -export default parseGrpcUrl; diff --git a/packages/insomnia-app/app/network/grpc/parse-grpc-url.ts b/packages/insomnia-app/app/network/grpc/parse-grpc-url.ts new file mode 100644 index 0000000000..2017210371 --- /dev/null +++ b/packages/insomnia-app/app/network/grpc/parse-grpc-url.ts @@ -0,0 +1,34 @@ +import url from 'url'; + +const parseGrpcUrl = ( + grpcUrl?: string, +): { + url: string; + enableTls: boolean; +} => { + const { protocol, host, href } = url.parse(grpcUrl?.toLowerCase() || ''); + + switch (protocol) { + case 'grpcs:': + return { + // @ts-expect-error -- TSCONVERSION host can be undefined + url: host, + enableTls: true, + }; + + case 'grpc:': + return { + // @ts-expect-error -- TSCONVERSION host can be undefined + url: host, + enableTls: false, + }; + + default: + return { + url: href, + enableTls: false, + }; + } +}; + +export default parseGrpcUrl; diff --git a/packages/insomnia-app/app/network/grpc/prepare.js b/packages/insomnia-app/app/network/grpc/prepare.ts similarity index 69% rename from packages/insomnia-app/app/network/grpc/prepare.js rename to packages/insomnia-app/app/network/grpc/prepare.ts index bfec8877cd..81e3500601 100644 --- a/packages/insomnia-app/app/network/grpc/prepare.js +++ b/packages/insomnia-app/app/network/grpc/prepare.ts @@ -1,4 +1,3 @@ -// @flow import type { RenderedGrpcRequest, RenderedGrpcRequestBody } from '../../common/render'; import type { GrpcMethodType } from './method'; import * as models from '../../models'; @@ -9,14 +8,14 @@ import { } from '../../common/render'; import { canClientStream } from './method'; -export type GrpcIpcRequestParams = { - request: RenderedGrpcRequest, -}; +export interface GrpcIpcRequestParams { + request: RenderedGrpcRequest; +} -export type GrpcIpcMessageParams = { - requestId: string, - body: RenderedGrpcRequestBody, -}; +export interface GrpcIpcMessageParams { + requestId: string; + body: RenderedGrpcRequestBody; +} export const prepareGrpcRequest = async ( requestId: string, @@ -25,16 +24,17 @@ export const prepareGrpcRequest = async ( ): Promise => { const req = await models.grpcRequest.getById(requestId); const environment = await models.environment.getById(environmentId || 'n/a'); - const request = await getRenderedGrpcRequest( + // @ts-expect-error -- TSCONVERSION req can be null but should not try to render if it is null req, environment, RENDER_PURPOSE_SEND, {}, canClientStream(methodType), ); - - return { request }; + return { + request, + }; }; export const prepareGrpcMessage = async ( @@ -43,13 +43,16 @@ export const prepareGrpcMessage = async ( ): Promise => { const req = await models.grpcRequest.getById(requestId); const environment = await models.environment.getById(environmentId || 'n/a'); - const requestBody = await getRenderedGrpcRequestMessage( + // @ts-expect-error -- TSCONVERSION req can be null but should not try to render if it is null req, environment, RENDER_PURPOSE_SEND, {}, ); - - return { body: requestBody, requestId: req._id }; + return { + body: requestBody, + // @ts-expect-error -- TSCONVERSION req can be null + requestId: req._id, + }; }; diff --git a/packages/insomnia-app/app/network/grpc/proto-loader/__mocks__/index.js b/packages/insomnia-app/app/network/grpc/proto-loader/__mocks__/index.ts similarity index 100% rename from packages/insomnia-app/app/network/grpc/proto-loader/__mocks__/index.js rename to packages/insomnia-app/app/network/grpc/proto-loader/__mocks__/index.ts diff --git a/packages/insomnia-app/app/network/grpc/proto-loader/__tests__/index.test.js b/packages/insomnia-app/app/network/grpc/proto-loader/__tests__/index.test.ts similarity index 79% rename from packages/insomnia-app/app/network/grpc/proto-loader/__tests__/index.test.js rename to packages/insomnia-app/app/network/grpc/proto-loader/__tests__/index.test.ts index c86785a664..2d88f46b07 100644 --- a/packages/insomnia-app/app/network/grpc/proto-loader/__tests__/index.test.js +++ b/packages/insomnia-app/app/network/grpc/proto-loader/__tests__/index.test.ts @@ -1,5 +1,4 @@ -// @flow -import * as protoLoader from '../index'; +import { loadMethods } from '../index'; import writeProtoFile from '../write-proto-file'; import path from 'path'; import { globalBeforeEach } from '../../../../__jest__/before-each'; @@ -12,7 +11,6 @@ jest.mock('../write-proto-file', () => ({ describe('loadMethods', () => { const protoFilePath = path.join(__dirname, '../../__fixtures__/library/hello.proto'); - beforeEach(() => { globalBeforeEach(); jest.resetAllMocks(); @@ -24,12 +22,12 @@ describe('loadMethods', () => { parentId: w._id, protoText: 'this is just a placeholder because writing to a file is mocked', }); - writeProtoFile.mockResolvedValue({ filePath: protoFilePath, dirs: [] }); - - const methods = await protoLoader.loadMethods(pf); - + writeProtoFile.mockResolvedValue({ + filePath: protoFilePath, + dirs: [], + }); + const methods = await loadMethods(pf); expect(writeProtoFile).toHaveBeenCalledWith(pf); - expect(methods).toHaveLength(4); expect(methods.map(c => c.path)).toStrictEqual( expect.arrayContaining([ @@ -47,8 +45,7 @@ describe('loadMethods', () => { parentId: w._id, protoText: '', }); - - await expect(protoLoader.loadMethods(undefined)).resolves.toHaveLength(0); - await expect(protoLoader.loadMethods(pf)).resolves.toHaveLength(0); + await expect(loadMethods(undefined)).resolves.toHaveLength(0); + await expect(loadMethods(pf)).resolves.toHaveLength(0); }); }); diff --git a/packages/insomnia-app/app/network/grpc/proto-loader/__tests__/write-proto-file.test.js b/packages/insomnia-app/app/network/grpc/proto-loader/__tests__/write-proto-file.test.ts similarity index 97% rename from packages/insomnia-app/app/network/grpc/proto-loader/__tests__/write-proto-file.test.js rename to packages/insomnia-app/app/network/grpc/proto-loader/__tests__/write-proto-file.test.ts index 8137f251ad..ea54378eac 100644 --- a/packages/insomnia-app/app/network/grpc/proto-loader/__tests__/write-proto-file.test.js +++ b/packages/insomnia-app/app/network/grpc/proto-loader/__tests__/write-proto-file.test.ts @@ -1,4 +1,3 @@ -// @flow import path from 'path'; import os from 'os'; import fs from 'fs'; @@ -8,30 +7,30 @@ import writeProtoFile from '../write-proto-file'; import mkdirp from 'mkdirp'; describe('writeProtoFile', () => { - let mkdirpSyncSpy: * | JestMockFn<*, *>; - let tmpDirSpy: * | JestMockFn<*, *>; - let existsSyncSpy: * | JestMockFn<*, *>; - let writeFileSpy: * | JestMockFn<*, Promise<*>>; + let existsSyncSpy: jest.SpyInstance; + let mkdirpSyncSpy: jest.SpyInstance; + let tmpDirSpy: jest.SpyInstance; + let writeFileSpy: jest.SpyInstance; const _setupSpies = () => { - tmpDirSpy = jest.spyOn(os, 'tmpdir'); existsSyncSpy = jest.spyOn(fs, 'existsSync'); mkdirpSyncSpy = jest.spyOn(mkdirp, 'sync'); + tmpDirSpy = jest.spyOn(os, 'tmpdir'); writeFileSpy = jest.spyOn(fs.promises, 'writeFile'); }; const _configureSpies = (tmpDir: string, exists: boolean) => { - mkdirpSyncSpy.mockImplementation(() => {}); - writeFileSpy.mockResolvedValue(); - tmpDirSpy.mockReturnValue(tmpDir); existsSyncSpy.mockReturnValue(exists); + mkdirpSyncSpy.mockImplementation(() => {}); + tmpDirSpy.mockReturnValue(tmpDir); + writeFileSpy.mockResolvedValue(undefined); }; const _restoreSpies = () => { - tmpDirSpy.mockRestore(); existsSyncSpy.mockRestore(); - writeFileSpy.mockRestore(); mkdirpSyncSpy.mockRestore(); + tmpDirSpy.mockRestore(); + writeFileSpy.mockRestore(); }; beforeEach(async () => { @@ -43,6 +42,7 @@ describe('writeProtoFile', () => { afterEach(() => { _restoreSpies(); + jest.resetAllMocks(); }); @@ -54,25 +54,23 @@ describe('writeProtoFile', () => { parentId: w._id, protoText: 'text', }); - const tmpDirPath = path.join('.', 'foo', 'bar', 'baz'); + _configureSpies(tmpDirPath, false); // file doesn't already exist // Act const result = await writeProtoFile(pf); - // Assert const expectedDir = path.join(tmpDirPath, 'insomnia-grpc'); const expectedFileName = `${pf._id}.${pf.modified}.proto`; const expectedFullPath = path.join(expectedDir, expectedFileName); - expect(result.filePath).toEqual(expectedFileName); expect(result.dirs).toEqual([expectedDir]); - expect(mkdirpSyncSpy).toHaveBeenCalledWith(expectedDir); expect(existsSyncSpy).toHaveBeenCalledWith(expectedFullPath); expect(writeFileSpy).toHaveBeenCalledWith(expectedFullPath, pf.protoText); }); + it('doesnt write individual file if it already exists', async () => { // Arrange const w = await models.workspace.create(); @@ -80,21 +78,18 @@ describe('writeProtoFile', () => { parentId: w._id, protoText: 'text', }); - const tmpDirPath = path.join('.', 'foo', 'bar', 'baz'); + _configureSpies(tmpDirPath, true); // file already exists // Act const result = await writeProtoFile(pf); - // Assert const expectedDir = path.join(tmpDirPath, 'insomnia-grpc'); const expectedFileName = `${pf._id}.${pf.modified}.proto`; const expectedFullPath = path.join(expectedDir, expectedFileName); - expect(result.filePath).toEqual(expectedFileName); expect(result.dirs).toEqual([expectedDir]); - expect(mkdirpSyncSpy).toHaveBeenCalledWith(expectedDir); expect(existsSyncSpy).toHaveBeenCalledWith(expectedFullPath); expect(writeFileSpy).not.toHaveBeenCalled(); @@ -114,13 +109,12 @@ describe('writeProtoFile', () => { name: 'hello.proto', protoText: 'text', }); - const tmpDirPath = path.join('.', 'foo', 'bar', 'baz'); + _configureSpies(tmpDirPath, false); // file doesn't already exist // Act const result = await writeProtoFile(pf); - // Assert const expectedRootDir = path.join( tmpDirPath, @@ -130,14 +124,13 @@ describe('writeProtoFile', () => { ); const expectedFilePath = pf.name; const expectedFullPath = path.join(expectedRootDir, expectedFilePath); - expect(result.filePath).toEqual(expectedFilePath); expect(result.dirs).toEqual([expectedRootDir]); - expect(mkdirpSyncSpy).toHaveBeenCalledWith(expectedRootDir); expect(existsSyncSpy).toHaveBeenCalledWith(expectedFullPath); expect(writeFileSpy).toHaveBeenCalledWith(expectedFullPath, pf.protoText); }); + it('can write files contained in nested folders', async () => { // Arrange const w = await models.workspace.create(); @@ -159,13 +152,12 @@ describe('writeProtoFile', () => { name: 'nested.proto', protoText: 'nested', }); - const tmpDirPath = path.join('.', 'foo', 'bar', 'baz'); + _configureSpies(tmpDirPath, false); // files don't already exist // Act const result = await writeProtoFile(pfNested); - // Assert const expectedRootDir = path.join( tmpDirPath, @@ -174,7 +166,6 @@ describe('writeProtoFile', () => { pdRoot.name, ); const expectedNestedDir = path.join(expectedRootDir, pdNested.name); - const expectedFilePath = { root: pfRoot.name, nested: path.join(pdNested.name, pfNested.name), @@ -183,20 +174,18 @@ describe('writeProtoFile', () => { root: path.join(expectedRootDir, expectedFilePath.root), nested: path.join(expectedRootDir, expectedFilePath.nested), }; - expect(result.filePath).toEqual(expectedFilePath.nested); expect(result.dirs).toEqual([expectedRootDir, expectedNestedDir]); - // Root folder should be created and written to expect(mkdirpSyncSpy).toHaveBeenCalledWith(expectedRootDir); expect(existsSyncSpy).toHaveBeenCalledWith(expectedFullPath.root); expect(writeFileSpy).toHaveBeenCalledWith(expectedFullPath.root, pfRoot.protoText); - // Nested folder should be created and written to expect(mkdirpSyncSpy).toHaveBeenCalledWith(expectedNestedDir); expect(existsSyncSpy).toHaveBeenCalledWith(expectedFullPath.nested); expect(writeFileSpy).toHaveBeenCalledWith(expectedFullPath.nested, pfNested.protoText); }); + it('should not write file if it already exists', async () => { // Arrange const w = await models.workspace.create(); @@ -218,13 +207,12 @@ describe('writeProtoFile', () => { name: 'nested.proto', protoText: 'nested', }); - const tmpDirPath = path.join('.', 'foo', 'bar', 'baz'); + _configureSpies(tmpDirPath, true); // files already exists // Act const result = await writeProtoFile(pfNested); - // Assert const expectedRootDir = path.join( tmpDirPath, @@ -233,7 +221,6 @@ describe('writeProtoFile', () => { pdRoot.name, ); const expectedNestedDir = path.join(expectedRootDir, pdNested.name); - const expectedFilePath = { root: pfRoot.name, nested: path.join(pdNested.name, pfNested.name), @@ -242,16 +229,12 @@ describe('writeProtoFile', () => { root: path.join(expectedRootDir, expectedFilePath.root), nested: path.join(expectedRootDir, expectedFilePath.nested), }; - expect(result.filePath).toEqual(expectedFilePath.nested); expect(result.dirs).toEqual([expectedRootDir, expectedNestedDir]); - expect(mkdirpSyncSpy).toHaveBeenCalledWith(expectedRootDir); expect(existsSyncSpy).toHaveBeenCalledWith(expectedFullPath.root); - expect(mkdirpSyncSpy).toHaveBeenCalledWith(expectedNestedDir); expect(existsSyncSpy).toHaveBeenCalledWith(expectedFullPath.nested); - expect(writeFileSpy).not.toHaveBeenCalled(); }); }); diff --git a/packages/insomnia-app/app/network/grpc/proto-loader/index.js b/packages/insomnia-app/app/network/grpc/proto-loader/index.ts similarity index 65% rename from packages/insomnia-app/app/network/grpc/proto-loader/index.js rename to packages/insomnia-app/app/network/grpc/proto-loader/index.ts index 2cf35ba255..d84eb25640 100644 --- a/packages/insomnia-app/app/network/grpc/proto-loader/index.js +++ b/packages/insomnia-app/app/network/grpc/proto-loader/index.ts @@ -1,9 +1,10 @@ -// @flow - import type { GrpcMethodDefinition } from '../method'; import * as protoLoader from '@grpc/proto-loader'; +import { AnyDefinition, EnumTypeDefinition, ServiceDefinition, MessageTypeDefinition } from '@grpc/proto-loader'; import * as models from '../../../models'; import writeProtoFile from './write-proto-file'; +import { ProtoFile } from '../../../models/proto-file'; +import { GrpcRequest } from '../../../models/grpc-request'; const GRPC_LOADER_OPTIONS = { keepCase: true, @@ -13,13 +14,14 @@ const GRPC_LOADER_OPTIONS = { oneofs: true, }; -const isTypeOrEnumDefinition = (obj: Object) => 'format' in obj; // same check exists internally in the grpc library -const isServiceDefinition = (obj: Object) => !isTypeOrEnumDefinition(obj); +const isTypeOrEnumDefinition = (obj: AnyDefinition): obj is EnumTypeDefinition | MessageTypeDefinition => 'format' in obj; // same check exists internally in the grpc library + +const isServiceDefinition = (obj: AnyDefinition): obj is ServiceDefinition => !isTypeOrEnumDefinition(obj); // TODO: The file path for protoLoader.load can also be a URL, so we can avoid // writing to a file in those cases, but it becomes more important to cache export const loadMethods = async ( - protoFile: ProtoFile | undefined, + protoFile?: ProtoFile | null, ): Promise> => { if (!protoFile?.protoText) { return []; @@ -33,14 +35,8 @@ export const loadMethodsFromPath = async ( filePath: string, includeDirs?: Array, ): Promise> => { - const definition = await protoLoader.load(filePath, { - ...GRPC_LOADER_OPTIONS, - includeDirs, - }); - - return Object.values(definition) - .filter(isServiceDefinition) - .flatMap(Object.values); + const definition = await protoLoader.load(filePath, { ...GRPC_LOADER_OPTIONS, includeDirs }); + return Object.values(definition).filter(isServiceDefinition).flatMap(Object.values); }; // TODO: instead of reloading the methods from the protoFile, @@ -48,10 +44,10 @@ export const loadMethodsFromPath = async ( // or from the cache // We can't send the method over IPC because of the following deprecation in Electron v9 // https://www.electronjs.org/docs/breaking-changes#behavior-changed-sending-non-js-objects-over-ipc-now-throws-an-exception +// @ts-expect-error -- TSCONVERSION export const getSelectedMethod = async (request: GrpcRequest): GrpcMethodDefinition | undefined => { + // @ts-expect-error -- TSCONVERSION const protoFile = await models.protoFile.getById(request.protoFileId); - const methods = await loadMethods(protoFile); - return methods.find(c => c.path === request.protoMethodName); }; diff --git a/packages/insomnia-app/app/network/grpc/proto-loader/write-proto-file.js b/packages/insomnia-app/app/network/grpc/proto-loader/write-proto-file.ts similarity index 87% rename from packages/insomnia-app/app/network/grpc/proto-loader/write-proto-file.js rename to packages/insomnia-app/app/network/grpc/proto-loader/write-proto-file.ts index 7f105d71a3..95129d4185 100644 --- a/packages/insomnia-app/app/network/grpc/proto-loader/write-proto-file.js +++ b/packages/insomnia-app/app/network/grpc/proto-loader/write-proto-file.ts @@ -1,35 +1,37 @@ -// @flow import path from 'path'; import os from 'os'; import mkdirp from 'mkdirp'; import fs from 'fs'; import type { ProtoFile } from '../../../models/proto-file'; import type { ProtoDirectory } from '../../../models/proto-directory'; -import * as db from '../../../common/database'; +import { database as db } from '../../../common/database'; import * as models from '../../../models'; import { isProtoDirectory, isProtoFile, isWorkspace } from '../../../models/helpers/is-model'; import type { BaseModel } from '../../../models'; import type { Workspace } from '../../../models/workspace'; const getProtoTempFileName = ({ _id, modified }: ProtoFile): string => `${_id}.${modified}.proto`; + const getProtoTempDirectoryName = ({ _id, modified }: ProtoDirectory): string => `${_id}.${modified}`; -type WriteResult = { - filePath: string, - dirs: Array, -}; +interface WriteResult { + filePath: string; + dirs: Array; +} const writeIndividualProtoFile = async (protoFile: ProtoFile): Promise => { // Create temp folder const rootDir = path.join(os.tmpdir(), 'insomnia-grpc'); mkdirp.sync(rootDir); - const filePath = getProtoTempFileName(protoFile); - const result = { filePath, dirs: [rootDir] }; - + const result = { + filePath, + dirs: [rootDir], + }; // Check if file already exists const fullPath = path.join(rootDir, filePath); + if (fs.existsSync(fullPath)) { return result; } @@ -39,9 +41,10 @@ const writeIndividualProtoFile = async (protoFile: ProtoFile): Promise => { +const writeNestedProtoFile = async (protoFile: ProtoFile, dirPath: string) => { // Check if file already exists const fullPath = path.join(dirPath, protoFile.name); + if (fs.existsSync(fullPath)) { return; } @@ -58,9 +61,15 @@ const writeProtoFileTree = async ( // Find the root ancestor directory const rootAncestorProtoDirectory = ancestors.find( + // @ts-expect-error -- TSCONVERSION ancestor workspace can be undefined c => isProtoDirectory(c) && c.parentId === ancestorWorkspace._id, ); + if (!ancestorWorkspace || !rootAncestorProtoDirectory) { + // should never happen + return []; + } + // Find all descendants of the root ancestor directory const descendants = await db.withDescendants(rootAncestorProtoDirectory); @@ -88,17 +97,14 @@ const recursiveWriteProtoDirectory = async ( // Increment folder path const dirPath = path.join(currentDirPath, dir.name); mkdirp.sync(dirPath); - // Get and write proto files - const files = descendants.filter(f => isProtoFile(f) && f.parentId === dir._id); + const files = descendants.filter(isProtoFile).filter(f => f.parentId === dir._id); await Promise.all(files.map(f => writeNestedProtoFile(f, dirPath))); - // Get and write subdirectories const subDirs = descendants.filter(f => isProtoDirectory(f) && f.parentId === dir._id); const createdDirs = await Promise.all( subDirs.map(f => recursiveWriteProtoDirectory(f, descendants, dirPath)), ); - return [dirPath, ...createdDirs.flat()]; }; @@ -108,7 +114,6 @@ const writeProtoFile = async (protoFile: ProtoFile): Promise => { models.protoDirectory.type, models.workspace.type, ]); - const ancestorDirectories = ancestors.filter(isProtoDirectory); // Is this file part of a directory? @@ -121,7 +126,10 @@ const writeProtoFile = async (protoFile: ProtoFile): Promise => { .reverse() .slice(1); const filePath = path.join(...subDirs, protoFile.name); - return { filePath, dirs: treeRootDirs }; + return { + filePath, + dirs: treeRootDirs, + }; } else { // Write single file return writeIndividualProtoFile(protoFile); diff --git a/packages/insomnia-app/app/network/grpc/proto-manager/__tests__/index.test.js b/packages/insomnia-app/app/network/grpc/proto-manager/__tests__/index.test.ts similarity index 80% rename from packages/insomnia-app/app/network/grpc/proto-manager/__tests__/index.test.js rename to packages/insomnia-app/app/network/grpc/proto-manager/__tests__/index.test.ts index d81896835f..845036df2a 100644 --- a/packages/insomnia-app/app/network/grpc/proto-manager/__tests__/index.test.js +++ b/packages/insomnia-app/app/network/grpc/proto-manager/__tests__/index.test.ts @@ -1,26 +1,25 @@ -// @flow - import fs from 'fs'; import path from 'path'; import { globalBeforeEach } from '../../../../__jest__/before-each'; import selectFileOrFolder from '../../../../common/select-file-or-folder'; import * as protoManager from '../index'; -import * as protoLoader from '../../proto-loader'; +import { loadMethods as _loadMethods, loadMethodsFromPath as _loadMethodsFromPath } from '../../proto-loader'; import * as models from '../../../../models'; import * as modals from '../../../../ui/components/modals'; -import * as db from '../../../../common/database'; +import { database as db } from '../../../../common/database'; + +const loadMethods = _loadMethods as jest.Mock; +const loadMethodsFromPath = _loadMethodsFromPath as jest.Mock; jest.mock('../../../../common/select-file-or-folder', () => ({ __esModule: true, default: jest.fn(), })); - jest.mock('../../../../ui/components/modals'); jest.mock('../../proto-loader'); describe('protoManager', () => { - const selectFileOrFolderMock: JestMockFn<*, *> = selectFileOrFolder; - + const selectFileOrFolderMock = selectFileOrFolder as jest.Mock; beforeEach(() => { globalBeforeEach(); jest.resetAllMocks(); @@ -31,7 +30,9 @@ describe('protoManager', () => { // Arrange const cbMock = jest.fn(); const w = await models.workspace.create(); - selectFileOrFolderMock.mockResolvedValue({ canceled: true }); + selectFileOrFolderMock.mockResolvedValue({ + canceled: true, + }); // Act await protoManager.addFile(w._id, cbMock); @@ -40,7 +41,6 @@ describe('protoManager', () => { expect(cbMock).not.toHaveBeenCalled(); const pf = await models.protoFile.getByParentId(w._id); expect(pf).toBeNull(); - expect(selectFileOrFolderMock).toHaveBeenCalledWith({ itemTypes: ['file'], extensions: ['proto'], @@ -61,8 +61,9 @@ describe('protoManager', () => { expect(cbMock).not.toHaveBeenCalled(); const pf = await models.protoFile.getByParentId(w._id); expect(pf).toBeNull(); - - expect(modals.showError).toHaveBeenCalledWith({ error }); + expect(modals.showError).toHaveBeenCalledWith({ + error, + }); }); it('should not create database entry if methods cannot be parsed', async () => { @@ -71,8 +72,10 @@ describe('protoManager', () => { const w = await models.workspace.create(); const error = new Error(); const filePath = 'path'; - selectFileOrFolderMock.mockResolvedValue({ filePath }); - protoLoader.loadMethodsFromPath.mockRejectedValue(error); + selectFileOrFolderMock.mockResolvedValue({ + filePath, + }); + loadMethodsFromPath.mockRejectedValue(error); // Act await protoManager.addFile(w._id, cbMock); @@ -81,7 +84,6 @@ describe('protoManager', () => { expect(cbMock).not.toHaveBeenCalled(); const pf = await models.protoFile.getByParentId(w._id); expect(pf).toBeNull(); - expect(modals.showError).toHaveBeenCalledWith({ title: 'Invalid Proto File', message: `The file ${filePath} and could not be parsed`, @@ -94,9 +96,10 @@ describe('protoManager', () => { const cbMock = jest.fn(); const w = await models.workspace.create(); const filePath = 'filename.proto'; - selectFileOrFolderMock.mockResolvedValue({ filePath }); - protoLoader.loadMethodsFromPath.mockResolvedValue(); - + selectFileOrFolderMock.mockResolvedValue({ + filePath, + }); + loadMethodsFromPath.mockResolvedValue(undefined); const fsReadFileSpy = jest.spyOn(fs.promises, 'readFile'); const contents = 'contents'; fsReadFileSpy.mockResolvedValue(contents); @@ -107,7 +110,6 @@ describe('protoManager', () => { // Assert const pf = await models.protoFile.getByParentId(w._id); expect(cbMock).toHaveBeenCalledWith(pf._id); - expect(pf.name).toBe(filePath); expect(pf.protoText).toBe(contents); }); @@ -118,12 +120,14 @@ describe('protoManager', () => { // Arrange const cbMock = jest.fn(); const w = await models.workspace.create(); - const pf = await models.protoFile.create({ parentId: w._id }); - + const pf = await models.protoFile.create({ + parentId: w._id, + }); const filePath = 'filename.proto'; - selectFileOrFolderMock.mockResolvedValue({ filePath }); - protoLoader.loadMethodsFromPath.mockResolvedValue(); - + selectFileOrFolderMock.mockResolvedValue({ + filePath, + }); + loadMethodsFromPath.mockResolvedValue(undefined); const fsReadFileSpy = jest.spyOn(fs.promises, 'readFile'); const contents = 'contents'; fsReadFileSpy.mockResolvedValue(contents); @@ -133,7 +137,6 @@ describe('protoManager', () => { // Assert expect(cbMock).toHaveBeenCalledWith(pf._id); - const updatedPf = await models.protoFile.getById(pf._id); expect(updatedPf.name).toBe(filePath); expect(updatedPf.protoText).toBe(contents); @@ -144,7 +147,10 @@ describe('protoManager', () => { it('should rename the file', async () => { // Arrange const w = await models.workspace.create(); - const pf = await models.protoFile.create({ parentId: w._id, name: 'original' }); + const pf = await models.protoFile.create({ + parentId: w._id, + name: 'original', + }); // Act const updatedName = 'updated'; @@ -160,12 +166,15 @@ describe('protoManager', () => { it('should alert the user before deleting a file', async () => { // Arrange const w = await models.workspace.create(); - const pf = await models.protoFile.create({ parentId: w._id, name: 'pfName.proto' }); + const pf = await models.protoFile.create({ + parentId: w._id, + name: 'pfName.proto', + }); const cbMock = jest.fn(); // Act await protoManager.deleteFile(pf, cbMock); - const showAlertCallArg = (modals.showAlert: JestMockFn).mock.calls[0][0]; + const showAlertCallArg = (modals.showAlert as jest.Mock).mock.calls[0][0]; expect(showAlertCallArg.title).toBe('Delete pfName.proto'); await showAlertCallArg.onConfirm(); @@ -179,15 +188,23 @@ describe('protoManager', () => { it('should alert the user before deleting a directory', async () => { // Arrange const w = await models.workspace.create(); - const pd = await models.protoDirectory.create({ parentId: w._id, name: 'pdName' }); - const pf1 = await models.protoFile.create({ parentId: pd._id, name: 'pfName1.proto' }); - const pf2 = await models.protoFile.create({ parentId: pd._id, name: 'pfName2.proto' }); - + const pd = await models.protoDirectory.create({ + parentId: w._id, + name: 'pdName', + }); + const pf1 = await models.protoFile.create({ + parentId: pd._id, + name: 'pfName1.proto', + }); + const pf2 = await models.protoFile.create({ + parentId: pd._id, + name: 'pfName2.proto', + }); const cbMock = jest.fn(); // Act await protoManager.deleteDirectory(pd, cbMock); - const showAlertCallArg = (modals.showAlert: JestMockFn).mock.calls[0][0]; + const showAlertCallArg = (modals.showAlert as jest.Mock).mock.calls[0][0]; expect(showAlertCallArg.title).toBe('Delete pdName'); await showAlertCallArg.onConfirm(); @@ -200,14 +217,12 @@ describe('protoManager', () => { }); describe('addDirectory', () => { - let dbBufferChangesIndefinitelySpy: * | JestMockFn<*, *>; - let dbFlushChangesSpy: * | JestMockFn<*, *>; - + let dbBufferChangesIndefinitelySpy: any | jest.Mock; + let dbFlushChangesSpy: any | jest.Mock; beforeEach(() => { dbBufferChangesIndefinitelySpy = jest.spyOn(db, 'bufferChangesIndefinitely'); dbFlushChangesSpy = jest.spyOn(db, 'flushChanges'); }); - afterEach(() => { expect(dbBufferChangesIndefinitelySpy).toHaveBeenCalled(); expect(dbFlushChangesSpy).toHaveBeenCalled(); @@ -218,7 +233,9 @@ describe('protoManager', () => { it('should not create database entries if loading canceled', async () => { // Arrange const w = await models.workspace.create(); - selectFileOrFolderMock.mockResolvedValue({ canceled: true }); + selectFileOrFolderMock.mockResolvedValue({ + canceled: true, + }); // Act await protoManager.addDirectory(w._id); @@ -240,9 +257,9 @@ describe('protoManager', () => { // Assert await expect(models.protoDirectory.all()).resolves.toHaveLength(0); await expect(models.protoFile.all()).resolves.toHaveLength(0); - - expect(modals.showError).toHaveBeenCalledWith({ error }); - + expect(modals.showError).toHaveBeenCalledWith({ + error, + }); expect(dbFlushChangesSpy).toHaveBeenCalledWith(expect.any(Number), true); }); @@ -250,7 +267,9 @@ describe('protoManager', () => { // Arrange const w = await models.workspace.create(); const filePath = path.join(__dirname, '../../__fixtures__/', 'library', 'empty'); - selectFileOrFolderMock.mockResolvedValue({ filePath }); + selectFileOrFolderMock.mockResolvedValue({ + filePath, + }); // Act await protoManager.addDirectory(w._id); @@ -258,7 +277,6 @@ describe('protoManager', () => { // Assert await expect(models.protoDirectory.all()).resolves.toHaveLength(0); await expect(models.protoFile.all()).resolves.toHaveLength(0); - expect(modals.showAlert).toHaveBeenCalledWith({ title: 'No files found', message: `No .proto files were found under ${filePath}.`, @@ -269,7 +287,9 @@ describe('protoManager', () => { // Arrange const w = await models.workspace.create(); const filePath = path.join(__dirname, '../../__fixtures__/', 'simulate-error'); - selectFileOrFolderMock.mockResolvedValue({ filePath }); + selectFileOrFolderMock.mockResolvedValue({ + filePath, + }); // Should error when loading the 6th file const error = new Error('should-error.proto could not be loaded'); @@ -291,13 +311,11 @@ describe('protoManager', () => { // Expect rollback expect(dbFlushChangesSpy).toHaveBeenCalledWith(expect.any(Number), true); - expect(modals.showError).toHaveBeenCalledWith({ title: 'Failed to import', message: `An unexpected error occurred when reading ${filePath}`, error, }); - fsPromisesReadFileSpy.mockRestore(); }); @@ -305,9 +323,11 @@ describe('protoManager', () => { // Arrange const w = await models.workspace.create(); const filePath = path.join(__dirname, '../../__fixtures__/', 'library', 'nested', 'time'); - selectFileOrFolderMock.mockResolvedValue({ filePath }); + selectFileOrFolderMock.mockResolvedValue({ + filePath, + }); const error = new Error('error'); - protoLoader.loadMethods.mockRejectedValue(error); + loadMethods.mockRejectedValue(error); // Act await protoManager.addDirectory(w._id); @@ -315,13 +335,11 @@ describe('protoManager', () => { // Assert await expect(models.protoDirectory.all()).resolves.toHaveLength(0); await expect(models.protoFile.all()).resolves.toHaveLength(0); - expect(modals.showError).toHaveBeenCalledWith({ title: 'Invalid Proto File', - message: `The file time.proto could not be parsed`, + message: 'The file time.proto could not be parsed', error, }); - expect(dbFlushChangesSpy).toHaveBeenCalledWith(expect.any(Number), true); }); @@ -329,16 +347,16 @@ describe('protoManager', () => { // Arrange const w = await models.workspace.create(); const filePath = path.join(__dirname, '../../__fixtures__/', 'library'); - selectFileOrFolderMock.mockResolvedValue({ filePath }); + selectFileOrFolderMock.mockResolvedValue({ + filePath, + }); // Act await protoManager.addDirectory(w._id); // Assert await expect(models.protoDirectory.all()).resolves.toHaveLength(3); - await expect(models.protoFile.all()).resolves.toHaveLength(3); - - // Each individual entry is not validated here because it is + await expect(models.protoFile.all()).resolves.toHaveLength(3); // Each individual entry is not validated here because it is // too involved to mock everything, and an integration test exists // which uses this code path. As long as the expected number of // entities are loaded from the fixture directory, this test is sufficient. diff --git a/packages/insomnia-app/app/network/grpc/proto-manager/__tests__/ingest-proto-directory.test.js b/packages/insomnia-app/app/network/grpc/proto-manager/__tests__/ingest-proto-directory.test.ts similarity index 85% rename from packages/insomnia-app/app/network/grpc/proto-manager/__tests__/ingest-proto-directory.test.js rename to packages/insomnia-app/app/network/grpc/proto-manager/__tests__/ingest-proto-directory.test.ts index f57ce201c5..2616acb184 100644 --- a/packages/insomnia-app/app/network/grpc/proto-manager/__tests__/ingest-proto-directory.test.js +++ b/packages/insomnia-app/app/network/grpc/proto-manager/__tests__/ingest-proto-directory.test.ts @@ -1,4 +1,3 @@ -// @flow import * as models from '../../../../models'; import ingestProtoDirectory from '../ingest-proto-directory'; import path from 'path'; @@ -13,12 +12,14 @@ describe('ingestProtoDirectory', () => { // Arrange const w = await models.workspace.create(); const dirToIngest = path.join(__dirname, '../../__fixtures__', 'library', dir); - // Act const result = await ingestProtoDirectory(dirToIngest, w._id); - // Assert - expect(result).toStrictEqual({ createdDir: null, createdIds: [], error: null }); + expect(result).toStrictEqual({ + createdDir: null, + createdIds: [], + error: null, + }); expect(models.protoDirectory.all()).resolves.toHaveLength(0); expect(models.protoFile.all()).resolves.toHaveLength(0); }, @@ -28,37 +29,37 @@ describe('ingestProtoDirectory', () => { // Arrange const w = await models.workspace.create(); const dirToIngest = path.join(__dirname, '../../__fixtures__', 'library'); - // Act const result = await ingestProtoDirectory(dirToIngest, w._id); - // Assert expect(result.createdDir).toStrictEqual( - expect.objectContaining({ name: 'library', parentId: w._id }), + expect.objectContaining({ + name: 'library', + parentId: w._id, + }), ); expect(result.createdIds).toHaveLength(6); expect(result.error).toBeNull(); expect(models.protoDirectory.all()).resolves.toHaveLength(3); expect(models.protoFile.all()).resolves.toHaveLength(3); - // Ensure ingested tree structure is correct const libraryFolder = await models.protoDirectory.getByParentId(w._id); expect(libraryFolder.name).toBe('library'); - const protos = await models.protoFile.findByParentId(libraryFolder._id); expect(protos).toStrictEqual( expect.arrayContaining([ - expect.objectContaining({ name: 'hello.proto' }), - expect.objectContaining({ name: 'root.proto' }), + expect.objectContaining({ + name: 'hello.proto', + }), + expect.objectContaining({ + name: 'root.proto', + }), ]), ); - const nestedFolder = await models.protoDirectory.getByParentId(libraryFolder._id); expect(nestedFolder.name).toBe('nested'); - const timeFolder = await models.protoDirectory.getByParentId(nestedFolder._id); expect(timeFolder.name).toBe('time'); - const timeProto = await models.protoFile.getByParentId(timeFolder._id); expect(timeProto.name).toBe('time.proto'); }); diff --git a/packages/insomnia-app/app/network/grpc/proto-manager/index.js b/packages/insomnia-app/app/network/grpc/proto-manager/index.tsx similarity index 86% rename from packages/insomnia-app/app/network/grpc/proto-manager/index.js rename to packages/insomnia-app/app/network/grpc/proto-manager/index.tsx index 6df3ba0402..7895193686 100644 --- a/packages/insomnia-app/app/network/grpc/proto-manager/index.js +++ b/packages/insomnia-app/app/network/grpc/proto-manager/index.tsx @@ -1,11 +1,9 @@ -// @flow - import type { ProtoFile } from '../../../models/proto-file'; import { showAlert, showError } from '../../../ui/components/modals'; import * as models from '../../../models'; import React from 'react'; import type { ProtoDirectory } from '../../../models/proto-directory'; -import * as db from '../../../common/database'; +import { database as db } from '../../../common/database'; import selectFileOrFolder from '../../../common/select-file-or-folder'; import ingestProtoDirectory from './ingest-proto-directory'; import fs from 'fs'; @@ -13,7 +11,7 @@ import path from 'path'; import * as protoLoader from '../proto-loader'; import { isProtoFile } from '../../../models/helpers/is-model'; -export async function deleteFile(protoFile: ProtoFile, callback: string => void): Promise { +export async function deleteFile(protoFile: ProtoFile, callback: (arg0: string) => void) { showAlert({ title: `Delete ${protoFile.name}`, message: ( @@ -30,10 +28,7 @@ export async function deleteFile(protoFile: ProtoFile, callback: string => void) }); } -export async function deleteDirectory( - protoDirectory: ProtoDirectory, - callback: (Array) => void, -): Promise { +export async function deleteDirectory(protoDirectory: ProtoDirectory, callback: (arg0: Array) => void) { showAlert({ title: `Delete ${protoDirectory.name}`, message: ( @@ -46,17 +41,16 @@ export async function deleteDirectory( onConfirm: async () => { const descendant = await db.withDescendants(protoDirectory); await models.protoDirectory.remove(protoDirectory); - callback(descendant.map(c => c._id)); }, }); } -export async function addDirectory(workspaceId: string): Promise { +export async function addDirectory(workspaceId: string) { let rollback = false; let createdIds: Array; - const bufferId = await db.bufferChangesIndefinitely(); + try { // Select file const { filePath, canceled } = await selectFileOrFolder({ @@ -105,14 +99,15 @@ export async function addDirectory(workspaceId: string): Promise { message: `The file ${file.name} could not be parsed`, error: e, }); - rollback = true; return; } } } catch (e) { rollback = true; - showError({ error: e }); + showError({ + error: e, + }); } finally { // Fake flushing changes (or, rollback) only prevents change notifs being sent to the UI // It does NOT revert changes written to the database, as is typical of a db transaction rollback @@ -120,13 +115,15 @@ export async function addDirectory(workspaceId: string): Promise { await db.flushChanges(bufferId, rollback); if (rollback) { + // @ts-expect-error -- TSCONVERSION await models.protoDirectory.batchRemoveIds(createdIds); + // @ts-expect-error -- TSCONVERSION await models.protoFile.batchRemoveIds(createdIds); } } } -async function _readFile(): Promise<{ fileName: string, fileContents: string } | null> { +async function _readFile() { try { // Select file const { filePath, canceled } = await selectFileOrFolder({ @@ -148,22 +145,27 @@ async function _readFile(): Promise<{ fileName: string, fileContents: string } | message: `The file ${filePath} and could not be parsed`, error: e, }); - return; } // Read contents const contents = await fs.promises.readFile(filePath, 'utf-8'); const name = path.basename(filePath); - - return { fileName: name, fileContents: contents }; + return { + fileName: name, + fileContents: contents, + }; } catch (e) { - showError({ error: e }); + showError({ + error: e, + }); } + return undefined; } -export async function addFile(workspaceId: string, callback: string => void): Promise { +export async function addFile(workspaceId: string, callback: (arg0: string) => void) { const result = await _readFile(); + if (result) { const newFile = await models.protoFile.create({ name: result.fileName, @@ -174,8 +176,9 @@ export async function addFile(workspaceId: string, callback: string => void): Pr } } -export async function updateFile(protoFile: ProtoFile, callback: string => void): Promise { +export async function updateFile(protoFile: ProtoFile, callback: (arg0: string) => void) { const result = await _readFile(); + if (result) { const updatedFile = await models.protoFile.update(protoFile, { name: result.fileName, @@ -185,6 +188,8 @@ export async function updateFile(protoFile: ProtoFile, callback: string => void) } } -export async function renameFile(protoFile: ProtoFile, name: string): Promise { - await models.protoFile.update(protoFile, { name }); +export async function renameFile(protoFile: ProtoFile, name: string) { + await models.protoFile.update(protoFile, { + name, + }); } diff --git a/packages/insomnia-app/app/network/grpc/proto-manager/ingest-proto-directory.js b/packages/insomnia-app/app/network/grpc/proto-manager/ingest-proto-directory.ts similarity index 76% rename from packages/insomnia-app/app/network/grpc/proto-manager/ingest-proto-directory.js rename to packages/insomnia-app/app/network/grpc/proto-manager/ingest-proto-directory.ts index 3d4411455c..e8f58dfcf9 100644 --- a/packages/insomnia-app/app/network/grpc/proto-manager/ingest-proto-directory.js +++ b/packages/insomnia-app/app/network/grpc/proto-manager/ingest-proto-directory.ts @@ -1,29 +1,30 @@ -// @flow - import * as models from '../../../models'; import type { ProtoDirectory } from '../../../models/proto-directory'; import path from 'path'; import fs from 'fs'; -type IngestResult = { - createdDir?: ProtoDirectory | null, - createdIds: Array, - error?: Error, -}; +interface IngestResult { + createdDir?: ProtoDirectory | null; + createdIds: Array; + error?: Error | null; +} class ProtoDirectoryLoader { + createdIds: Array = []; + rootDirPath: string; + workspaceId: string; + constructor(rootDirPath: string, workspaceId: string) { this.rootDirPath = rootDirPath; this.workspaceId = workspaceId; - this.createdIds = []; } - async _parseDir(entryPath: string, parentId: string): Promise { + async _parseDir(entryPath: string, parentId: string) { const result = await this._ingest(entryPath, parentId); return Boolean(result); } - async _parseFile(entryPath: string, parentId: string): Promise { + async _parseFile(entryPath: string, parentId: string) { const extension = path.extname(entryPath); // Ignore if not a .proto file @@ -33,15 +34,12 @@ class ProtoDirectoryLoader { const contents = await fs.promises.readFile(entryPath, 'utf-8'); const name = path.basename(entryPath); - const { _id } = await models.protoFile.create({ name, parentId, protoText: contents, }); - this.createdIds.push(_id); - return true; } @@ -52,18 +50,18 @@ class ProtoDirectoryLoader { } const newDirId = models.protoDirectory.createId(); - // Read contents - const entries = await fs.promises.readdir(dirPath, { withFileTypes: true }); - + const entries = await fs.promises.readdir(dirPath, { + withFileTypes: true, + }); // Loop and read all entries let filesFound = false; + for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) { const fullEntryPath = path.resolve(dirPath, entry.name); const result = await (entry.isDirectory() ? this._parseDir(fullEntryPath, newDirId) : this._parseFile(fullEntryPath, newDirId)); - filesFound = filesFound || result; } @@ -81,12 +79,20 @@ class ProtoDirectoryLoader { return null; } - async load(): Promise { + async load() { try { const createdDir = await this._ingest(this.rootDirPath, this.workspaceId); - return { createdDir, createdIds: this.createdIds, error: null }; + return { + createdDir, + createdIds: this.createdIds, + error: null, + } as IngestResult; } catch (error) { - return { createdDir: null, createdIds: this.createdIds, error }; + return { + createdDir: null, + createdIds: this.createdIds, + error, + } as IngestResult; } } } diff --git a/packages/insomnia-app/app/network/grpc/response-callbacks.js b/packages/insomnia-app/app/network/grpc/response-callbacks.ts similarity index 76% rename from packages/insomnia-app/app/network/grpc/response-callbacks.js rename to packages/insomnia-app/app/network/grpc/response-callbacks.ts index b88c2e371d..366e954099 100644 --- a/packages/insomnia-app/app/network/grpc/response-callbacks.js +++ b/packages/insomnia-app/app/network/grpc/response-callbacks.ts @@ -1,17 +1,15 @@ -// @flow -import type { GrpcStatusObject, ServiceError } from './service-error'; import { GrpcResponseEventEnum } from '../../common/grpc-events'; - +import { IpcMainEvent } from 'electron'; +import { ServiceError, StatusObject } from '@grpc/grpc-js'; interface IResponseCallbacks { - sendData(requestId: string, val: Object | undefined): void; + sendData(requestId: string, val: Record | undefined): void; sendError(requestId: string, err: ServiceError): void; sendStart(requestId: string): void; sendEnd(requestId: string): void; - sendStatus(requestId: string, status: GrpcStatusObject): void; + sendStatus(requestId: string, status: StatusObject): void; } - export class ResponseCallbacks implements IResponseCallbacks { - _event: IpcMainEvent = null; + _event: IpcMainEvent; constructor(e: IpcMainEvent) { this._event = e; diff --git a/packages/insomnia-app/app/network/grpc/service-error.js b/packages/insomnia-app/app/network/grpc/service-error.ts similarity index 61% rename from packages/insomnia-app/app/network/grpc/service-error.js rename to packages/insomnia-app/app/network/grpc/service-error.ts index 65af74a242..6b00abff2e 100644 --- a/packages/insomnia-app/app/network/grpc/service-error.js +++ b/packages/insomnia-app/app/network/grpc/service-error.ts @@ -1,4 +1,4 @@ -// @flow +import { ValueOf } from 'type-fest'; export const GrpcStatusEnum = { OK: 0, @@ -18,14 +18,14 @@ export const GrpcStatusEnum = { UNAVAILABLE: 14, DATA_LOSS: 15, UNAUTHENTICATED: 16, -}; +} as const; -type GrpcStatus = $Values; +type GrpcStatus = ValueOf; -export type GrpcStatusObject = { - code: GrpcStatus, - details: string, - metadata: Object, // https://grpc.github.io/grpc/node/grpc.Metadata.html -}; +export interface GrpcStatusObject { + code: GrpcStatus; + details: string; + metadata: Record; // https://grpc.github.io/grpc/node/grpc.Metadata.html +} export type ServiceError = GrpcStatusObject & Error; diff --git a/packages/insomnia-app/app/network/multipart.js b/packages/insomnia-app/app/network/multipart.ts similarity index 89% rename from packages/insomnia-app/app/network/multipart.js rename to packages/insomnia-app/app/network/multipart.ts index 5822855a82..66dfef7ac0 100644 --- a/packages/insomnia-app/app/network/multipart.js +++ b/packages/insomnia-app/app/network/multipart.ts @@ -1,4 +1,3 @@ -// @flow import * as electron from 'electron'; import mimes from 'mime-types'; import fs from 'fs'; @@ -7,16 +6,23 @@ import type { RequestBodyParameter } from '../models/request'; export const DEFAULT_BOUNDARY = 'X-INSOMNIA-BOUNDARY'; +interface Multipart { + boundary: typeof DEFAULT_BOUNDARY, + filePath: string; + contentLength: number; +} + export async function buildMultipart(params: Array) { - return new Promise(async (resolve: Function, reject: Function) => { + return new Promise(async (resolve, reject) => { const filePath = path.join(electron.remote.app.getPath('temp'), Math.random() + '.body'); const writeStream = fs.createWriteStream(filePath); const lineBreak = '\r\n'; let totalSize = 0; - function addFile(path: string): Promise { - return new Promise((resolve, reject) => { + function addFile(path: string) { + return new Promise((resolve, reject) => { let size; + try { size = fs.statSync(path).size; } catch (err) { @@ -27,12 +33,12 @@ export async function buildMultipart(params: Array) { stream.once('end', () => { resolve(); }); - stream.once('error', err => { reject(err); }); - - stream.pipe(writeStream, { end: false }); + stream.pipe(writeStream, { + end: false, + }); totalSize += size; }); } @@ -67,6 +73,7 @@ export async function buildMultipart(params: Array) { addString(`Content-Type: ${contentType}`); addString(lineBreak); addString(lineBreak); + try { await addFile(fileName); } catch (err) { @@ -78,10 +85,12 @@ export async function buildMultipart(params: Array) { const contentType = param.multiline; addString(`Content-Disposition: form-data; name="${name}"`); addString(lineBreak); + if (typeof contentType === 'string') { addString(`Content-Type: ${contentType}`); addString(lineBreak); } + addString(lineBreak); addString(value); } @@ -91,11 +100,9 @@ export async function buildMultipart(params: Array) { addString(`--${DEFAULT_BOUNDARY}--`); addString(lineBreak); - writeStream.once('error', err => { reject(err); }); - writeStream.once('close', () => { resolve({ boundary: DEFAULT_BOUNDARY, @@ -103,7 +110,6 @@ export async function buildMultipart(params: Array) { contentLength: totalSize, }); }); - // We're done here. End the stream and tell FS to save/close the file. writeStream.end(); }); diff --git a/packages/insomnia-app/app/network/network.js b/packages/insomnia-app/app/network/network.ts similarity index 88% rename from packages/insomnia-app/app/network/network.js rename to packages/insomnia-app/app/network/network.ts index cba4121c2b..144466a80d 100644 --- a/packages/insomnia-app/app/network/network.js +++ b/packages/insomnia-app/app/network/network.ts @@ -1,4 +1,3 @@ -// @flow import type { ResponseHeader, ResponseTimelineEntry } from '../models/response'; import type { Request, RequestHeader } from '../models/request'; import type { Workspace } from '../models/workspace'; @@ -33,7 +32,6 @@ import { CONTENT_TYPE_FORM_DATA, CONTENT_TYPE_FORM_URLENCODED, getAppVersion, - getTempDir, HttpVersions, STATUS_CODE_PLUGIN_ERROR, } from '../common/constants'; @@ -41,7 +39,6 @@ import { delay, describeByteSize, getContentTypeHeader, - getDataDirectory, getHostHeader, getLocationHeader, getSetCookieHeaders, @@ -52,6 +49,7 @@ import { hasUserAgentHeader, waitForStreamToFinish, } from '../common/misc'; +import { getDataDirectory, getTempDir } from '../common/electron-helpers'; import { buildQueryStringFromParams, joinUrlAndQueryString, @@ -59,8 +57,8 @@ import { smartEncodeUrl, } from 'insomnia-url'; import fs from 'fs'; -import * as db from '../common/database'; -import CACerts from './ca-certs'; +import { database as db } from '../common/database'; +import * as caCerts from './ca-certs'; import * as plugins from '../plugins/index'; import * as pluginContexts from '../plugins/context/index'; import { getAuthHeader } from './authentication'; @@ -69,27 +67,28 @@ import { urlMatchesCertHost } from './url-matches-cert-host'; import aws4 from 'aws4'; import { buildMultipart } from './multipart'; import type { Environment } from '../models/environment'; +import { CurlOptionName } from 'node-libcurl/dist/generated/CurlOption'; -export type ResponsePatch = {| - bodyCompression?: 'zip' | null, - bodyPath?: string, - bytesContent?: number, - bytesRead?: number, - contentType?: string, - elapsedTime?: number, - environmentId?: string | null, - error?: string, - headers?: Array, - httpVersion?: string, - message?: string, - parentId?: string, - settingSendCookies?: boolean, - settingStoreCookies?: boolean, - statusCode?: number, - statusMessage?: string, - timelinePath?: string, - url?: string, -|}; +export interface ResponsePatch { + bodyCompression?: 'zip' | null; + bodyPath?: string; + bytesContent?: number; + bytesRead?: number; + contentType?: string; + elapsedTime?: number; + environmentId?: string | null; + error?: string; + headers?: Array; + httpVersion?: string; + message?: string; + parentId?: string; + settingSendCookies?: boolean; + settingStoreCookies?: boolean; + statusCode?: number; + statusMessage?: string; + timelinePath?: string; + url?: string; +} // Time since user's last keypress to wait before making the request const MAX_DELAY_TIME = 1000; @@ -110,15 +109,18 @@ const LIBCURL_DEBUG_MIGRATION_MAP = { }; const cancelRequestFunctionMap = {}; + let lastUserInteraction = Date.now(); export async function cancelRequestById(requestId) { if (hasCancelFunctionForId(requestId)) { const cancelRequestFunction = cancelRequestFunctionMap[requestId]; + if (typeof cancelRequestFunction === 'function') { return cancelRequestFunction(); } } + console.log(`[network] Failed to cancel req=${requestId} because cancel function not found`); } @@ -134,12 +136,12 @@ export function hasCancelFunctionForId(requestId) { export async function _actuallySend( renderedRequest: RenderedRequest, - renderContext: Object, + renderContext: Record, workspace: Workspace, settings: Settings, environment: Environment | null, -): Promise { - return new Promise(async resolve => { +) { + return new Promise(async resolve => { const timeline: Array = []; function addTimeline(name, value) { @@ -161,24 +163,23 @@ export async function _actuallySend( async function respond( patch: ResponsePatch, bodyPath: string | null, - noPlugins: boolean = false, - ): Promise { + noPlugins = false, + ) { const timelinePath = await storeTimeline(timeline); - // Tear Down the cancellation logic clearCancelFunctionForId(renderedRequest._id); - const environmentId = environment ? environment._id : null; const responsePatchBeforeHooks = Object.assign( - ({ + { timelinePath, environmentId, parentId: renderedRequest._id, - bodyCompression: null, // Will default to .zip otherwise + bodyCompression: null, + // Will default to .zip otherwise bodyPath: bodyPath || '', settingSendCookies: renderedRequest.settingSendCookies, settingStoreCookies: renderedRequest.settingStoreCookies, - }: ResponsePatch), + } as ResponsePatch, patch, ); @@ -187,7 +188,8 @@ export async function _actuallySend( return; } - let responsePatch: ?ResponsePatch; + let responsePatch: ResponsePatch | null = null; + try { responsePatch = await _applyResponsePluginHooks( responsePatchBeforeHooks, @@ -205,7 +207,7 @@ export async function _actuallySend( } /** Helper function to respond with an error */ - async function handleError(err: Error): Promise { + async function handleError(err: Error) { await respond( { url: renderedRequest.url, @@ -222,11 +224,12 @@ export async function _actuallySend( } /** Helper function to set Curl options */ - function setOpt(opt: number, val: any, optional: boolean = false) { - const name = Object.keys(Curl.option).find(name => Curl.option[name] === opt); + function setOpt(opt: Parameters[0] | CurlOptionName, val: Parameters[1], optional = false) { try { + // @ts-expect-error -- TSCONVERSION curl.setOpt(opt, val); } catch (err) { + const name = Object.keys(Curl.option).find(name => Curl.option[name] === opt); if (!optional) { throw new Error(`${err.message} (${opt} ${name || 'n/a'})`); } else { @@ -244,8 +247,10 @@ export async function _actuallySend( cancelRequestFunctionMap[renderedRequest._id] = async () => { await respond( { - elapsedTime: curl.getInfo(Curl.info.TOTAL_TIME) * 1000, + elapsedTime: (curl.getInfo(Curl.info.TOTAL_TIME) as number || 0) * 1000, + // @ts-expect-error -- needs generic bytesRead: curl.getInfo(Curl.info.SIZE_DOWNLOAD), + // @ts-expect-error -- needs generic url: curl.getInfo(Curl.info.EFFECTIVE_URL), statusMessage: 'Cancelled', error: 'Request was cancelled', @@ -253,15 +258,20 @@ export async function _actuallySend( null, true, ); - // Kill it! curl.close(); }; // Set all the basic options - setOpt(Curl.option.VERBOSE, true); // True so debug function works - setOpt(Curl.option.NOPROGRESS, true); // True so curl doesn't print progress - setOpt(Curl.option.ACCEPT_ENCODING, ''); // Auto decode everything + setOpt(Curl.option.VERBOSE, true); + + // True so debug function works\ + setOpt(Curl.option.NOPROGRESS, true); + + // True so curl doesn't print progress + setOpt(Curl.option.ACCEPT_ENCODING, ''); + + // Auto decode everything enable(CurlFeature.Raw); // Set follow redirects setting @@ -269,9 +279,11 @@ export async function _actuallySend( case 'off': setOpt(Curl.option.FOLLOWLOCATION, false); break; + case 'on': setOpt(Curl.option.FOLLOWLOCATION, true); break; + default: // Set to global setting setOpt(Curl.option.FOLLOWLOCATION, settings.followRedirects); @@ -296,26 +308,31 @@ export async function _actuallySend( // This is how you tell Curl to send a HEAD request setOpt(Curl.option.NOBODY, 1); break; + case 'POST': // This is how you tell Curl to send a POST request setOpt(Curl.option.POST, 1); break; + default: // IMPORTANT: Only use CUSTOMREQUEST for all but HEAD and POST setOpt(Curl.option.CUSTOMREQUEST, renderedRequest.method); break; } + // @ts-expect-error -- TSCONVERSION appears to be a genuine error // Setup debug handler setOpt(Curl.option.DEBUGFUNCTION, (infoType: string, contentBuffer: Buffer) => { const content = contentBuffer.toString('utf8'); const rawName = Object.keys(CurlInfoDebug).find(k => CurlInfoDebug[k] === infoType) || ''; const name = LIBCURL_DEBUG_MIGRATION_MAP[rawName] || rawName; + // @ts-expect-error -- TSCONVERSION appears to be a genuine error if (infoType === CurlInfoDebug.SslDataIn || infoType === CurlInfoDebug.SslDataOut) { return 0; } + // @ts-expect-error -- TSCONVERSION appears to be a genuine error // Ignore the possibly large data messages if (infoType === CurlInfoDebug.DataOut) { if (contentBuffer.length === 0) { @@ -325,32 +342,33 @@ export async function _actuallySend( } else { addTimeline(name, `(${describeByteSize(contentBuffer.length)} hidden)`); } + return 0; } + // @ts-expect-error -- TSCONVERSION appears to be a genuine error if (infoType === CurlInfoDebug.DataIn) { addTimelineText(`Received ${describeByteSize(contentBuffer.length)} chunk`); return 0; } + // @ts-expect-error -- TSCONVERSION appears to be a genuine error // Don't show cookie setting because this will display every domain in the jar if (infoType === CurlInfoDebug.Text && content.indexOf('Added cookie') === 0) { return 0; } addTimeline(name, content); - return 0; // Must be here }); - // Set the headers (to be modified as we go) const headers = clone(renderedRequest.headers); - // Set the URL, including the query parameters const qs = buildQueryStringFromParams(renderedRequest.parameters); const url = joinUrlAndQueryString(renderedRequest.url, qs); const isUnixSocket = url.match(/https?:\/\/unix:\//); const finalUrl = smartEncodeUrl(url, renderedRequest.settingEncodeUrl); + if (isUnixSocket) { // URL prep will convert "unix:/path" hostname to "unix/path" const match = finalUrl.match(/(https?:)\/\/unix:?(\/[^:]+):\/(.+)/); @@ -362,6 +380,7 @@ export async function _actuallySend( } else { setOpt(Curl.option.URL, finalUrl); } + addTimelineText('Preparing request to ' + finalUrl); addTimelineText('Current time is ' + new Date().toISOString()); addTimelineText(`Using ${Curl.getVersion()}`); @@ -372,21 +391,26 @@ export async function _actuallySend( addTimelineText('Using HTTP 1.0'); setOpt(Curl.option.HTTP_VERSION, CurlHttpVersion.V1_0); break; + case HttpVersions.V1_1: addTimelineText('Using HTTP 1.1'); setOpt(Curl.option.HTTP_VERSION, CurlHttpVersion.V1_1); break; + case HttpVersions.V2_0: addTimelineText('Using HTTP/2'); setOpt(Curl.option.HTTP_VERSION, CurlHttpVersion.V2_0); break; + case HttpVersions.v3: addTimelineText('Using HTTP/3'); setOpt(Curl.option.HTTP_VERSION, CurlHttpVersion.v3); break; + case HttpVersions.default: addTimelineText('Using default HTTP version'); break; + default: addTimelineText(`Unknown HTTP version specified ${settings.preferredHttpVersion}`); break; @@ -426,7 +450,8 @@ export async function _actuallySend( } catch (err) { // Doesn't exist yet, so write it mkdirp.sync(baseCAPath); - fs.writeFileSync(fullCAPath, CACerts); + // @ts-expect-error -- TSCONVERSION + fs.writeFileSync(fullCAPath, caCerts); console.log('[net] Set CA to', fullCAPath); } @@ -437,10 +462,11 @@ export async function _actuallySend( // Tell Curl to store cookies that it receives. This is only important if we receive // a cookie on a redirect that needs to be sent on the next request in the chain. curl.setOpt(Curl.option.COOKIEFILE, ''); - const cookies = renderedRequest.cookieJar.cookies || []; + for (const cookie of cookies) { let expiresTimestamp = 0; + if (cookie.expires) { const expiresDate = new Date(cookie.expires); expiresTimestamp = Math.round(expiresDate.getTime() / 1000); @@ -480,10 +506,12 @@ export async function _actuallySend( const proxyHost = protocol === 'https:' ? httpsProxy : httpProxy; const proxy = proxyHost ? setDefaultProtocol(proxyHost) : null; addTimelineText(`Enable network proxy for ${protocol || ''}`); + if (proxy) { setOpt(Curl.option.PROXY, proxy); setOpt(Curl.option.PROXYAUTH, CurlAuth.Any); } + if (noProxy) { setOpt(Curl.option.NOPROXY, noProxy); } @@ -493,7 +521,8 @@ export async function _actuallySend( // Set client certs if needed const clientCertificates = await models.clientCertificate.findByParentId(workspace._id); - for (const certificate of clientCertificates) { + + for (const certificate of (clientCertificates || [])) { if (certificate.disabled) { continue; } @@ -512,7 +541,6 @@ export async function _actuallySend( const name = `${renderedRequest._id}_${renderedRequest.modified}`; const fullPath = pathJoin(fullBase, name); fs.writeFileSync(fullPath, Buffer.from(blobOrFilename, 'base64')); - // Set filename to the one we just saved blobOrFilename = fullPath; } @@ -547,8 +575,9 @@ export async function _actuallySend( // Build the body let noBody = false; - let requestBody = null; + let requestBody: string | null = null; const expectsBody = ['POST', 'PUT', 'PATCH'].includes(renderedRequest.method.toUpperCase()); + if (renderedRequest.body.mimeType === CONTENT_TYPE_FORM_URLENCODED) { requestBody = buildQueryStringFromParams(renderedRequest.body.params || [], false); } else if (renderedRequest.body.mimeType === CONTENT_TYPE_FORM_DATA) { @@ -556,9 +585,9 @@ export async function _actuallySend( const { filePath: multipartBodyPath, boundary, contentLength } = await buildMultipart( params, ); - // Extend the Content-Type header const contentTypeHeader = getContentTypeHeader(headers); + if (contentTypeHeader) { contentTypeHeader.value = `multipart/form-data; boundary=${boundary}`; } else { @@ -569,11 +598,9 @@ export async function _actuallySend( } const fd = fs.openSync(multipartBodyPath, 'r'); - setOpt(Curl.option.INFILESIZE_LARGE, contentLength); setOpt(Curl.option.UPLOAD, 1); setOpt(Curl.option.READDATA, fd); - // We need this, otherwise curl will send it as a PUT setOpt(Curl.option.CUSTOMREQUEST, renderedRequest.method); @@ -590,15 +617,14 @@ export async function _actuallySend( const { size } = fs.statSync(renderedRequest.body.fileName); const fileName = renderedRequest.body.fileName || ''; const fd = fs.openSync(fileName, 'r'); - setOpt(Curl.option.INFILESIZE_LARGE, size); setOpt(Curl.option.UPLOAD, 1); setOpt(Curl.option.READDATA, fd); - // We need this, otherwise curl will send it as a POST setOpt(Curl.option.CUSTOMREQUEST, renderedRequest.method); const fn = () => fs.closeSync(fd); + curl.on('end', fn); curl.on('error', fn); } else if (typeof renderedRequest.body.mimeType === 'string' || expectsBody) { @@ -643,6 +669,7 @@ export async function _actuallySend( new Error('AWS authentication not supported for provided body type'), ); } + const { authentication } = renderedRequest; const credentials = { accessKeyId: authentication.accessKeyId || '', @@ -711,6 +738,7 @@ export async function _actuallySend( .filter(h => h.name) .map(h => { const value = h.value || ''; + if (value === '') { // Curl needs a semicolon suffix to send empty header values return `${h.name};`; @@ -722,8 +750,8 @@ export async function _actuallySend( return `${h.name}: ${value}`; } }); + // @ts-expect-error -- TSCONVERSION appears to be a genuine error setOpt(Curl.option.HTTPHEADER, headerStrings); - let responseBodyBytes = 0; const responsesDir = pathJoin(getDataDirectory(), 'responses'); mkdirp.sync(responsesDir); @@ -731,30 +759,27 @@ export async function _actuallySend( const responseBodyWriteStream = fs.createWriteStream(responseBodyPath); curl.on('end', () => responseBodyWriteStream.end()); curl.on('error', () => responseBodyWriteStream.end()); + // @ts-expect-error -- TSCONVERSION appears to be a genuine error setOpt(Curl.option.WRITEFUNCTION, (buff: Buffer) => { responseBodyBytes += buff.length; responseBodyWriteStream.write(buff); return buff.length; }); - // Handle the response ending - curl.on('end', async (_1, _2, rawHeaders) => { + curl.on('end', async (_1, _2, rawHeaders: Buffer) => { const allCurlHeadersObjects = _parseHeaders(rawHeaders); + // Headers are an array (one for each redirect) const lastCurlHeadersObject = allCurlHeadersObjects[allCurlHeadersObjects.length - 1]; - // Collect various things const httpVersion = lastCurlHeadersObject.version || ''; const statusCode = lastCurlHeadersObject.code || -1; const statusMessage = lastCurlHeadersObject.reason || ''; - // Collect the headers const headers = lastCurlHeadersObject.headers; - // Calculate the content type const contentTypeHeader = getContentTypeHeader(headers); const contentType = contentTypeHeader ? contentTypeHeader.value : ''; - // Update Cookie Jar let currentUrl = finalUrl; let setCookieStrings: Array = []; @@ -764,9 +789,9 @@ export async function _actuallySend( // Collect Set-Cookie headers const setCookieHeaders = getSetCookieHeaders(headers); setCookieStrings = [...setCookieStrings, ...setCookieHeaders.map(h => h.value)]; - // Pull out new URL if there is a redirect const newLocation = getLocationHeader(headers); + if (newLocation !== null) { currentUrl = urlResolve(currentUrl, newLocation.value); } @@ -784,12 +809,15 @@ export async function _actuallySend( // Update cookie jar if we need to and if we found any cookies if (renderedRequest.settingStoreCookies && setCookieStrings.length) { const cookies = await cookiesFromJar(jar); - await models.cookieJar.update(renderedRequest.cookieJar, { cookies }); + await models.cookieJar.update(renderedRequest.cookieJar, { + cookies, + }); } // Print informational message if (setCookieStrings.length > 0) { const n = setCookieStrings.length; + if (renderedRequest.settingStoreCookies) { addTimelineText(`Saved ${n} cookie${n === 1 ? '' : 's'}`); } else { @@ -798,28 +826,27 @@ export async function _actuallySend( } // Return the response data - const responsePatch = { + const responsePatch: ResponsePatch = { contentType, headers, httpVersion, statusCode, statusMessage, bytesContent: responseBodyBytes, + // @ts-expect-error -- TSCONVERSION appears to be a genuine error bytesRead: curl.getInfo(Curl.info.SIZE_DOWNLOAD), + // @ts-expect-error -- TSCONVERSION appears to be a genuine error elapsedTime: curl.getInfo(Curl.info.TOTAL_TIME) * 1000, + // @ts-expect-error -- TSCONVERSION appears to be a genuine error url: curl.getInfo(Curl.info.EFFECTIVE_URL), }; - // Close the request curl.close(); - // Make sure the response body has been fully written first await waitForStreamToFinish(responseBodyWriteStream); - // Send response await respond(responsePatch, responseBodyPath); }); - curl.on('error', async function(err, code) { let error = err + ''; let statusMessage = 'Error'; @@ -838,7 +865,6 @@ export async function _actuallySend( true, ); }); - curl.perform(); } catch (err) { console.log('[network] Error', err); @@ -849,9 +875,10 @@ export async function _actuallySend( export async function sendWithSettings( requestId: string, - requestPatch: Object, -): Promise { + requestPatch: Record, +) { const request = await models.request.getById(requestId); + if (!request) { throw new Error(`Failed to find request: ${requestId}`); } @@ -862,25 +889,26 @@ export async function sendWithSettings( models.requestGroup.type, models.workspace.type, ]); - const workspaceDoc = ancestors.find(doc => doc.type === models.workspace.type); const workspaceId = workspaceDoc ? workspaceDoc._id : 'n/a'; const workspace = await models.workspace.getById(workspaceId); + if (!workspace) { throw new Error(`Failed to find workspace for: ${requestId}`); } const workspaceMeta = await models.workspaceMeta.getOrCreateByParentId(workspace._id); const environmentId: string = workspaceMeta.activeEnvironmentId || 'n/a'; - const newRequest: Request = await models.initModel(models.request.type, requestPatch, { _id: request._id + '.other', parentId: request._id, }); - const environment: Environment | null = await models.environment.getById(environmentId || 'n/a'); + let renderResult: { + request: RenderedRequest; + context: Record; + }; - let renderResult: { request: RenderedRequest, context: Object }; try { renderResult = await getRenderedRequestAndContext(newRequest, environmentId); } catch (err) { @@ -900,10 +928,10 @@ export async function send( requestId: string, environmentId?: string, extraInfo?: ExtraRenderInfo, -): Promise { +) { console.log(`[network] Sending req=${requestId} env=${environmentId || 'null'}`); - // HACK: wait for all debounces to finish + /* * TODO: Do this in a more robust way * The following block adds a "long" delay to let potential debounces and @@ -913,6 +941,7 @@ export async function send( */ const timeSinceLastInteraction = Date.now() - lastUserInteraction; const delayMillis = Math.max(0, MAX_DELAY_TIME - timeSinceLastInteraction); + if (delayMillis > 0) { await delay(delayMillis); } @@ -931,24 +960,23 @@ export async function send( } const environment: Environment | null = await models.environment.getById(environmentId || 'n/a'); - const renderResult = await getRenderedRequestAndContext( request, environmentId || null, RENDER_PURPOSE_SEND, extraInfo, ); - const renderedRequestBeforePlugins = renderResult.request; const renderedContextBeforePlugins = renderResult.context; - const workspaceDoc = ancestors.find(doc => doc.type === models.workspace.type); const workspace = await models.workspace.getById(workspaceDoc ? workspaceDoc._id : 'n/a'); + if (!workspace) { throw new Error(`Failed to find workspace for request: ${requestId}`); } let renderedRequest: RenderedRequest; + try { renderedRequest = await _applyRequestPluginHooks( renderedRequestBeforePlugins, @@ -964,7 +992,7 @@ export async function send( statusCode: STATUS_CODE_PLUGIN_ERROR, statusMessage: err.plugin ? `Plugin ${err.plugin.name}` : 'Plugin', url: renderedRequestBeforePlugins.url, - }; + } as ResponsePatch; } const response = await _actuallySend( @@ -974,28 +1002,27 @@ export async function send( settings, environment, ); - console.log( response.error ? `[network] Response failed req=${requestId} err=${response.error || 'n/a'}` : `[network] Response succeeded req=${requestId} status=${response.statusCode || '?'}`, ); - return response; } async function _applyRequestPluginHooks( renderedRequest: RenderedRequest, - renderedContext: Object, -): Promise { + renderedContext: Record, +) { const newRenderedRequest = clone(renderedRequest); + for (const { plugin, hook } of await plugins.getRequestHooks()) { const context = { - ...(pluginContexts.app.init(RENDER_PURPOSE_NO_RENDER): Object), - ...(pluginContexts.data.init(): Object), - ...(pluginContexts.store.init(plugin): Object), - ...(pluginContexts.request.init(newRenderedRequest, renderedContext): Object), - ...(pluginContexts.network.init(renderedContext.getEnvironmentId()): Object), + ...(pluginContexts.app.init(RENDER_PURPOSE_NO_RENDER) as Record), + ...(pluginContexts.data.init() as Record), + ...(pluginContexts.store.init(plugin) as Record), + ...(pluginContexts.request.init(newRenderedRequest, renderedContext) as Record), + ...(pluginContexts.network.init(renderedContext.getEnvironmentId()) as Record), }; try { @@ -1012,19 +1039,19 @@ async function _applyRequestPluginHooks( async function _applyResponsePluginHooks( response: ResponsePatch, renderedRequest: RenderedRequest, - renderedContext: Object, -): Promise { + renderedContext: Record, +) { const newResponse = clone(response); const newRequest = clone(renderedRequest); for (const { plugin, hook } of await plugins.getResponseHooks()) { const context = { - ...(pluginContexts.app.init(RENDER_PURPOSE_NO_RENDER): Object), - ...(pluginContexts.data.init(): Object), - ...(pluginContexts.store.init(plugin): Object), - ...(pluginContexts.response.init(newResponse): Object), - ...(pluginContexts.request.init(newRequest, renderedContext, true): Object), - ...(pluginContexts.network.init(renderedContext.getEnvironmentId()): Object), + ...(pluginContexts.app.init(RENDER_PURPOSE_NO_RENDER) as Record), + ...(pluginContexts.data.init() as Record), + ...(pluginContexts.store.init(plugin) as Record), + ...(pluginContexts.response.init(newResponse) as Record), + ...(pluginContexts.request.init(newRequest, renderedContext, true) as Record), + ...(pluginContexts.network.init(renderedContext.getEnvironmentId()) as Record), }; try { @@ -1038,19 +1065,20 @@ async function _applyResponsePluginHooks( return newResponse; } +interface HeaderResult { + headers: Array; + version: string; + code: number; + reason: string; +} + export function _parseHeaders( buffer: Buffer, -): Array<{ - headers: Array, - version: string, - code: number, - reason: string, -}> { - const results = []; - +) { + const results: Array = []; const lines = buffer.toString('utf8').split(/\r?\n|\r/g); - for (let i = 0, currentResult = null; i < lines.length; i++) { + for (let i = 0, currentResult: HeaderResult | null = null; i < lines.length; i++) { const line = lines[i]; const isEmptyLine = line.trim() === ''; @@ -1085,9 +1113,9 @@ export function _parseHeaders( // exported for unit tests only export function _getAwsAuthHeaders( credentials: { - accessKeyId: string, - secretAccessKey: string, - sessionToken: string, + accessKeyId: string; + secretAccessKey: string; + sessionToken: string; }, headers: Array, body: string, @@ -1095,14 +1123,17 @@ export function _getAwsAuthHeaders( method: string, region?: string, service?: string, -): Array<{ name: string, value: string, description?: string, disabled?: boolean }> { +): Array<{ + name: string; + value: string; + description?: string; + disabled?: boolean; +}> { const parsedUrl = urlParse(url); const contentTypeHeader = getContentTypeHeader(headers); - // AWS uses host header for signing so prioritize that if the user set it manually const hostHeader = getHostHeader(headers); const host = hostHeader ? hostHeader.value : parsedUrl.host; - const awsSignOptions = { service, region, @@ -1110,11 +1141,13 @@ export function _getAwsAuthHeaders( body, method, path: parsedUrl.path, - headers: contentTypeHeader ? { 'content-type': contentTypeHeader.value } : {}, + headers: contentTypeHeader + ? { + 'content-type': contentTypeHeader.value, + } + : {}, }; - const signature = aws4.sign(awsSignOptions, credentials); - return Object.keys(signature.headers) .filter(name => name !== 'content-type') // Don't add this because we already have it .map(name => ({ @@ -1123,13 +1156,10 @@ export function _getAwsAuthHeaders( })); } -function storeTimeline(timeline: Array): Promise { - return new Promise((resolve, reject) => { +function storeTimeline(timeline: Array) { + return new Promise((resolve, reject) => { const timelineStr = JSON.stringify(timeline, null, '\t'); - const timelineHash = crypto - .createHash('sha1') - .update(timelineStr) - .digest('hex'); + const timelineHash = crypto.createHash('sha1').update(timelineStr).digest('hex'); const responsesDir = pathJoin(getDataDirectory(), 'responses'); mkdirp.sync(responsesDir); const timelinePath = pathJoin(responsesDir, timelineHash + '.timeline'); @@ -1151,8 +1181,7 @@ if (global.document) { lastUserInteraction = Date.now(); }); - - document.addEventListener('paste', (e: Event) => { + document.addEventListener('paste', () => { lastUserInteraction = Date.now(); }); } diff --git a/packages/insomnia-app/app/network/o-auth-1/constants.js b/packages/insomnia-app/app/network/o-auth-1/constants.ts similarity index 97% rename from packages/insomnia-app/app/network/o-auth-1/constants.js rename to packages/insomnia-app/app/network/o-auth-1/constants.ts index 7741afe1d1..f12ebe619f 100644 --- a/packages/insomnia-app/app/network/o-auth-1/constants.js +++ b/packages/insomnia-app/app/network/o-auth-1/constants.ts @@ -1,4 +1,3 @@ -// @flow export type OAuth1SignatureMethod = 'HMAC-SHA1' | 'RSA-SHA1' | 'HMAC-SHA256' | 'PLAINTEXT'; export const SIGNATURE_METHOD_HMAC_SHA1: OAuth1SignatureMethod = 'HMAC-SHA1'; export const SIGNATURE_METHOD_HMAC_SHA256: OAuth1SignatureMethod = 'HMAC-SHA256'; diff --git a/packages/insomnia-app/app/network/o-auth-1/get-token.js b/packages/insomnia-app/app/network/o-auth-1/get-token.ts similarity index 73% rename from packages/insomnia-app/app/network/o-auth-1/get-token.js rename to packages/insomnia-app/app/network/o-auth-1/get-token.ts index d0615bf867..52ee3ed34b 100644 --- a/packages/insomnia-app/app/network/o-auth-1/get-token.js +++ b/packages/insomnia-app/app/network/o-auth-1/get-token.ts @@ -1,4 +1,3 @@ -// @flow /** * Get an OAuth1Token object and also handle storing/saving/refreshing * @returns {Promise.} @@ -17,34 +16,25 @@ import { CONTENT_TYPE_FORM_URLENCODED } from '../../common/constants'; function hashFunction(signatureMethod: OAuth1SignatureMethod) { if (signatureMethod === SIGNATURE_METHOD_HMAC_SHA1) { - return function(baseString: string, key: string): string { - return crypto - .createHmac('sha1', key) - .update(baseString) - .digest('base64'); + return function(baseString: string, key: string) { + return crypto.createHmac('sha1', key).update(baseString).digest('base64'); }; } if (signatureMethod === SIGNATURE_METHOD_HMAC_SHA256) { - return function(baseString: string, key: string): string { - return crypto - .createHmac('sha256', key) - .update(baseString) - .digest('base64'); + return function(baseString: string, key: string) { + return crypto.createHmac('sha256', key).update(baseString).digest('base64'); }; } if (signatureMethod === SIGNATURE_METHOD_RSA_SHA1) { - return function(baseString: string, privatekey: string): string { - return crypto - .createSign('RSA-SHA1') - .update(baseString) - .sign(privatekey, 'base64'); + return function(baseString: string, privatekey: string) { + return crypto.createSign('RSA-SHA1').update(baseString).sign(privatekey, 'base64'); }; } if (signatureMethod === SIGNATURE_METHOD_PLAINTEXT) { - return function(baseString: string): string { + return function(baseString: string) { return baseString; }; } @@ -57,7 +47,7 @@ export default async function( method: string, authentication: RequestAuthentication, body: RequestBody | null = null, -): { [string]: string } { +) { const oauth = new OAuth1({ consumer: { key: authentication.consumerKey, @@ -68,7 +58,6 @@ export default async function( hash_function: hashFunction(authentication.signatureMethod), realm: authentication.realm || null, }); - const requestData = { url: url, method: method, @@ -79,40 +68,53 @@ export default async function( }; if (authentication.callback) { + // @ts-expect-error -- TSCONVERSION needs type widening requestData.data.oauth_callback = authentication.callback; } if (authentication.nonce) { + // @ts-expect-error -- TSCONVERSION needs type widening requestData.data.oauth_nonce = authentication.nonce; } if (authentication.timestamp) { + // @ts-expect-error -- TSCONVERSION needs type widening requestData.data.oauth_timestamp = authentication.timestamp; } if (authentication.verifier) { + // @ts-expect-error -- TSCONVERSION needs type widening requestData.data.oauth_verifier = authentication.verifier; } if (authentication.includeBodyHash && body && body.mimeType === CONTENT_TYPE_FORM_URLENCODED) { requestData.includeBodyHash = true; + for (const p of body.params || []) { requestData.data[p.name] = p.value; } } - let token = null; + let token: OAuth1.Token | undefined; + if (authentication.tokenKey && authentication.tokenSecret) { token = { key: authentication.tokenKey, secret: authentication.tokenSecret, }; } else if (authentication.tokenKey) { - token = { key: authentication.tokenKey }; + // @ts-expect-error -- TSCONVERSION likely needs a `secret: undefined` or the type is not actually correct. + token = { + key: authentication.tokenKey, + }; } if (authentication.signatureMethod === SIGNATURE_METHOD_RSA_SHA1) { - token = { key: authentication.tokenKey, secret: authentication.privateKey }; + token = { + key: authentication.tokenKey, + secret: authentication.privateKey, + }; + // We override getSigningKey for RSA-SHA1 because we don't want ddo/oauth-1.0a to percentEncode the token oauth.getSigningKey = function(tokenSecret) { return tokenSecret || ''; diff --git a/packages/insomnia-app/app/network/o-auth-2/__tests__/grant-authorization-code.test.js b/packages/insomnia-app/app/network/o-auth-2/__tests__/grant-authorization-code.test.ts similarity index 74% rename from packages/insomnia-app/app/network/o-auth-2/__tests__/grant-authorization-code.test.js rename to packages/insomnia-app/app/network/o-auth-2/__tests__/grant-authorization-code.test.ts index fe7c8700d8..b19c9d0665 100644 --- a/packages/insomnia-app/app/network/o-auth-2/__tests__/grant-authorization-code.test.js +++ b/packages/insomnia-app/app/network/o-auth-2/__tests__/grant-authorization-code.test.ts @@ -4,7 +4,7 @@ import { globalBeforeEach } from '../../../__jest__/before-each'; import * as network from '../../network'; import fs from 'fs'; import path from 'path'; -import { getTempDir } from '../../../common/constants'; +import { getTempDir } from '../../../common/electron-helpers'; // Mock some test things const AUTHORIZE_URL = 'https://foo.com/authorizeAuthCode'; @@ -19,10 +19,10 @@ const RESOURCE = 'foo.com'; describe('authorization_code', () => { beforeEach(globalBeforeEach); + it('gets token with JSON and basic auth', async () => { createBWRedirectMock(`${REDIRECT_URI}?code=code_123&state=${STATE}`); const bodyPath = path.join(getTempDir(), 'foo.response'); - fs.writeFileSync( bodyPath, JSON.stringify({ @@ -33,15 +33,18 @@ describe('authorization_code', () => { resource: RESOURCE, }), ); - network.sendWithSettings = jest.fn(() => ({ bodyPath, bodyCompression: '', parentId: 'req_1', statusCode: 200, - headers: [{ name: 'Content-Type', value: 'application/json' }], + headers: [ + { + name: 'Content-Type', + value: 'application/json', + }, + ], })); - const result = await getToken( 'req_1', AUTHORIZE_URL, @@ -55,7 +58,6 @@ describe('authorization_code', () => { AUDIENCE, RESOURCE, ); - // Check the request to fetch the token expect(network.sendWithSettings.mock.calls).toEqual([ [ @@ -66,12 +68,30 @@ describe('authorization_code', () => { body: { mimeType: 'application/x-www-form-urlencoded', params: [ - { name: 'grant_type', value: 'authorization_code' }, - { name: 'code', value: 'code_123' }, - { name: 'redirect_uri', value: REDIRECT_URI }, - { name: 'state', value: STATE }, - { name: 'audience', value: AUDIENCE }, - { name: 'resource', value: RESOURCE }, + { + name: 'grant_type', + value: 'authorization_code', + }, + { + name: 'code', + value: 'code_123', + }, + { + name: 'redirect_uri', + value: REDIRECT_URI, + }, + { + name: 'state', + value: STATE, + }, + { + name: 'audience', + value: AUDIENCE, + }, + { + name: 'resource', + value: RESOURCE, + }, ], }, headers: [ @@ -91,7 +111,6 @@ describe('authorization_code', () => { }, ], ]); - // Check the expected value expect(result).toEqual({ access_token: 'token_123', @@ -112,7 +131,6 @@ describe('authorization_code', () => { it('gets token with urlencoded and body auth', async () => { createBWRedirectMock(`${REDIRECT_URI}?code=code_123&state=${STATE}`); const bodyPath = path.join(getTempDir(), 'foo.response'); - fs.writeFileSync( bodyPath, JSON.stringify({ @@ -123,15 +141,18 @@ describe('authorization_code', () => { resource: RESOURCE, }), ); - network.sendWithSettings = jest.fn(() => ({ bodyPath, bodyCompression: '', parentId: 'req_1', statusCode: 200, - headers: [{ name: 'Content-Type', value: 'application/x-www-form-urlencoded' }], + headers: [ + { + name: 'Content-Type', + value: 'application/x-www-form-urlencoded', + }, + ], })); - const result = await getToken( 'req_1', AUTHORIZE_URL, @@ -145,7 +166,6 @@ describe('authorization_code', () => { AUDIENCE, RESOURCE, ); - // Check the request to fetch the token expect(network.sendWithSettings.mock.calls).toEqual([ [ @@ -156,14 +176,38 @@ describe('authorization_code', () => { body: { mimeType: 'application/x-www-form-urlencoded', params: [ - { name: 'grant_type', value: 'authorization_code' }, - { name: 'code', value: 'code_123' }, - { name: 'redirect_uri', value: REDIRECT_URI }, - { name: 'state', value: STATE }, - { name: 'audience', value: AUDIENCE }, - { name: 'resource', value: RESOURCE }, - { name: 'client_id', value: CLIENT_ID }, - { name: 'client_secret', value: CLIENT_SECRET }, + { + name: 'grant_type', + value: 'authorization_code', + }, + { + name: 'code', + value: 'code_123', + }, + { + name: 'redirect_uri', + value: REDIRECT_URI, + }, + { + name: 'state', + value: STATE, + }, + { + name: 'audience', + value: AUDIENCE, + }, + { + name: 'resource', + value: RESOURCE, + }, + { + name: 'client_id', + value: CLIENT_ID, + }, + { + name: 'client_secret', + value: CLIENT_SECRET, + }, ], }, headers: [ @@ -179,7 +223,6 @@ describe('authorization_code', () => { }, ], ]); - // Check the expected value expect(result).toEqual({ access_token: 'token_123', @@ -200,7 +243,6 @@ describe('authorization_code', () => { it('uses PKCE', async () => { createBWRedirectMock(`${REDIRECT_URI}?code=code_123&state=${STATE}`); const bodyPath = path.join(getTempDir(), 'foo.response'); - fs.writeFileSync( bodyPath, JSON.stringify({ @@ -211,15 +253,18 @@ describe('authorization_code', () => { resource: RESOURCE, }), ); - network.sendWithSettings = jest.fn(() => ({ bodyPath, bodyCompression: '', parentId: 'req_1', statusCode: 200, - headers: [{ name: 'Content-Type', value: 'application/json' }], + headers: [ + { + name: 'Content-Type', + value: 'application/json', + }, + ], })); - const result = await getToken( 'req_1', AUTHORIZE_URL, @@ -234,12 +279,14 @@ describe('authorization_code', () => { RESOURCE, true, ); - // Check the request to fetch the token expect(network.sendWithSettings.mock.calls[0][1].body.params).toEqual( - expect.arrayContaining([expect.objectContaining({ name: 'code_verifier' })]), + expect.arrayContaining([ + expect.objectContaining({ + name: 'code_verifier', + }), + ]), ); - // Check the expected value expect(result).toEqual({ access_token: 'token_123', diff --git a/packages/insomnia-app/app/network/o-auth-2/__tests__/grant-client-credentials.test.js b/packages/insomnia-app/app/network/o-auth-2/__tests__/grant-client-credentials.test.ts similarity index 75% rename from packages/insomnia-app/app/network/o-auth-2/__tests__/grant-client-credentials.test.js rename to packages/insomnia-app/app/network/o-auth-2/__tests__/grant-client-credentials.test.ts index cea7fd23ed..73b6cf880b 100644 --- a/packages/insomnia-app/app/network/o-auth-2/__tests__/grant-client-credentials.test.js +++ b/packages/insomnia-app/app/network/o-auth-2/__tests__/grant-client-credentials.test.ts @@ -3,7 +3,7 @@ import { globalBeforeEach } from '../../../__jest__/before-each'; import path from 'path'; import fs from 'fs'; import * as network from '../../network'; -import { getTempDir } from '../../../common/constants'; +import { getTempDir } from '../../../common/electron-helpers'; // Mock some test things const ACCESS_TOKEN_URL = 'https://foo.com/access_token'; @@ -15,9 +15,9 @@ const RESOURCE = 'https://foo.com/resource'; describe('client_credentials', () => { beforeEach(globalBeforeEach); + it('gets token with JSON and basic auth', async () => { const bodyPath = path.join(getTempDir(), 'foo.response'); - fs.writeFileSync( bodyPath, JSON.stringify({ @@ -28,15 +28,18 @@ describe('client_credentials', () => { resource: RESOURCE, }), ); - network.sendWithSettings = jest.fn(() => ({ bodyPath, bodyCompression: '', parentId: 'req_1', statusCode: 200, - headers: [{ name: 'Content-Type', value: 'application/json' }], + headers: [ + { + name: 'Content-Type', + value: 'application/json', + }, + ], })); - const result = await getToken( 'req_1', ACCESS_TOKEN_URL, @@ -47,7 +50,6 @@ describe('client_credentials', () => { AUDIENCE, RESOURCE, ); - // Check the request to fetch the token expect(network.sendWithSettings.mock.calls).toEqual([ [ @@ -58,10 +60,22 @@ describe('client_credentials', () => { body: { mimeType: 'application/x-www-form-urlencoded', params: [ - { name: 'grant_type', value: 'client_credentials' }, - { name: 'scope', value: SCOPE }, - { name: 'audience', value: AUDIENCE }, - { name: 'resource', value: RESOURCE }, + { + name: 'grant_type', + value: 'client_credentials', + }, + { + name: 'scope', + value: SCOPE, + }, + { + name: 'audience', + value: AUDIENCE, + }, + { + name: 'resource', + value: RESOURCE, + }, ], }, headers: [ @@ -81,7 +95,6 @@ describe('client_credentials', () => { }, ], ]); - // Check the expected value expect(result).toEqual({ access_token: 'token_123', @@ -100,7 +113,6 @@ describe('client_credentials', () => { it('gets token with urlencoded and body auth', async () => { const bodyPath = path.join(getTempDir(), 'req_1.response'); - fs.writeFileSync( bodyPath, JSON.stringify({ @@ -111,15 +123,18 @@ describe('client_credentials', () => { resource: RESOURCE, }), ); - network.sendWithSettings = jest.fn(() => ({ bodyPath, bodyCompression: '', parentId: 'req_1', statusCode: 200, - headers: [{ name: 'Content-Type', value: 'application/x-www-form-urlencoded' }], + headers: [ + { + name: 'Content-Type', + value: 'application/x-www-form-urlencoded', + }, + ], })); - const result = await getToken( 'req_1', ACCESS_TOKEN_URL, @@ -130,7 +145,6 @@ describe('client_credentials', () => { AUDIENCE, RESOURCE, ); - // Check the request to fetch the token expect(network.sendWithSettings.mock.calls).toEqual([ [ @@ -141,12 +155,30 @@ describe('client_credentials', () => { body: { mimeType: 'application/x-www-form-urlencoded', params: [ - { name: 'grant_type', value: 'client_credentials' }, - { name: 'scope', value: SCOPE }, - { name: 'audience', value: AUDIENCE }, - { name: 'resource', value: RESOURCE }, - { name: 'client_id', value: CLIENT_ID }, - { name: 'client_secret', value: CLIENT_SECRET }, + { + name: 'grant_type', + value: 'client_credentials', + }, + { + name: 'scope', + value: SCOPE, + }, + { + name: 'audience', + value: AUDIENCE, + }, + { + name: 'resource', + value: RESOURCE, + }, + { + name: 'client_id', + value: CLIENT_ID, + }, + { + name: 'client_secret', + value: CLIENT_SECRET, + }, ], }, headers: [ @@ -162,7 +194,6 @@ describe('client_credentials', () => { }, ], ]); - // Check the expected value expect(result).toEqual({ access_token: 'token_123', diff --git a/packages/insomnia-app/app/network/o-auth-2/__tests__/grant-implicit.test.js b/packages/insomnia-app/app/network/o-auth-2/__tests__/grant-implicit.test.ts similarity index 99% rename from packages/insomnia-app/app/network/o-auth-2/__tests__/grant-implicit.test.js rename to packages/insomnia-app/app/network/o-auth-2/__tests__/grant-implicit.test.ts index 8fc6e7b7d7..dc7f47abaf 100644 --- a/packages/insomnia-app/app/network/o-auth-2/__tests__/grant-implicit.test.js +++ b/packages/insomnia-app/app/network/o-auth-2/__tests__/grant-implicit.test.ts @@ -1,7 +1,6 @@ import getToken from '../grant-implicit'; import { createBWRedirectMock } from './helpers'; import { globalBeforeEach } from '../../../__jest__/before-each'; - // Mock some test things const AUTHORIZE_URL = 'https://foo.com/authorizeAuthCode'; const CLIENT_ID = 'client_123'; @@ -12,11 +11,10 @@ const STATE = 'state_123'; describe('implicit', () => { beforeEach(globalBeforeEach); + it('works in default case', async () => { createBWRedirectMock(`${REDIRECT_URI}#access_token=token_123&state=${STATE}&foo=bar`); - const result = await getToken(AUTHORIZE_URL, CLIENT_ID, REDIRECT_URI, SCOPE, STATE, AUDIENCE); - expect(result).toEqual({ access_token: 'token_123', id_token: null, diff --git a/packages/insomnia-app/app/network/o-auth-2/__tests__/grant-password.test.js b/packages/insomnia-app/app/network/o-auth-2/__tests__/grant-password.test.ts similarity index 72% rename from packages/insomnia-app/app/network/o-auth-2/__tests__/grant-password.test.js rename to packages/insomnia-app/app/network/o-auth-2/__tests__/grant-password.test.ts index 7860d43226..ff5b8c789f 100644 --- a/packages/insomnia-app/app/network/o-auth-2/__tests__/grant-password.test.js +++ b/packages/insomnia-app/app/network/o-auth-2/__tests__/grant-password.test.ts @@ -1,7 +1,7 @@ import getToken from '../grant-password'; import { globalBeforeEach } from '../../../__jest__/before-each'; import * as network from '../../network'; -import { getTempDir } from '../../../common/constants'; +import { getTempDir } from '../../../common/electron-helpers'; import path from 'path'; import fs from 'fs'; @@ -16,9 +16,9 @@ const AUDIENCE = 'https://foo.com/userinfo'; describe('password', () => { beforeEach(globalBeforeEach); + it('gets token with JSON and basic auth', async () => { const bodyPath = path.join(getTempDir(), 'foo.response'); - fs.writeFileSync( bodyPath, JSON.stringify({ @@ -28,15 +28,18 @@ describe('password', () => { audience: AUDIENCE, }), ); - network.sendWithSettings = jest.fn(() => ({ bodyPath, bodyCompression: '', parentId: 'req_1', statusCode: 200, - headers: [{ name: 'Content-Type', value: 'application/json' }], + headers: [ + { + name: 'Content-Type', + value: 'application/json', + }, + ], })); - const result = await getToken( 'req_1', ACCESS_TOKEN_URL, @@ -48,7 +51,6 @@ describe('password', () => { SCOPE, AUDIENCE, ); - // Check the request to fetch the token expect(network.sendWithSettings.mock.calls).toEqual([ [ @@ -59,11 +61,26 @@ describe('password', () => { body: { mimeType: 'application/x-www-form-urlencoded', params: [ - { name: 'grant_type', value: 'password' }, - { name: 'username', value: USERNAME }, - { name: 'password', value: PASSWORD }, - { name: 'scope', value: SCOPE }, - { name: 'audience', value: AUDIENCE }, + { + name: 'grant_type', + value: 'password', + }, + { + name: 'username', + value: USERNAME, + }, + { + name: 'password', + value: PASSWORD, + }, + { + name: 'scope', + value: SCOPE, + }, + { + name: 'audience', + value: AUDIENCE, + }, ], }, headers: [ @@ -83,7 +100,6 @@ describe('password', () => { }, ], ]); - // Check the expected value expect(result).toEqual({ access_token: 'token_123', @@ -102,7 +118,6 @@ describe('password', () => { it('gets token with urlencoded and body auth', async () => { const bodyPath = path.join(getTempDir(), 'foo.response'); - fs.writeFileSync( bodyPath, JSON.stringify({ @@ -112,15 +127,18 @@ describe('password', () => { audience: AUDIENCE, }), ); - network.sendWithSettings = jest.fn(() => ({ bodyPath, bodyCompression: '', parentId: 'req_1', statusCode: 200, - headers: [{ name: 'Content-Type', value: 'application/x-www-form-urlencoded' }], + headers: [ + { + name: 'Content-Type', + value: 'application/x-www-form-urlencoded', + }, + ], })); - const result = await getToken( 'req_1', ACCESS_TOKEN_URL, @@ -132,7 +150,6 @@ describe('password', () => { SCOPE, AUDIENCE, ); - // Check the request to fetch the token expect(network.sendWithSettings.mock.calls).toEqual([ [ @@ -143,13 +160,34 @@ describe('password', () => { body: { mimeType: 'application/x-www-form-urlencoded', params: [ - { name: 'grant_type', value: 'password' }, - { name: 'username', value: USERNAME }, - { name: 'password', value: PASSWORD }, - { name: 'scope', value: SCOPE }, - { name: 'audience', value: AUDIENCE }, - { name: 'client_id', value: CLIENT_ID }, - { name: 'client_secret', value: CLIENT_SECRET }, + { + name: 'grant_type', + value: 'password', + }, + { + name: 'username', + value: USERNAME, + }, + { + name: 'password', + value: PASSWORD, + }, + { + name: 'scope', + value: SCOPE, + }, + { + name: 'audience', + value: AUDIENCE, + }, + { + name: 'client_id', + value: CLIENT_ID, + }, + { + name: 'client_secret', + value: CLIENT_SECRET, + }, ], }, headers: [ @@ -165,7 +203,6 @@ describe('password', () => { }, ], ]); - // Check the expected value expect(result).toEqual({ access_token: 'token_123', diff --git a/packages/insomnia-app/app/network/o-auth-2/__tests__/helpers.js b/packages/insomnia-app/app/network/o-auth-2/__tests__/helpers.ts similarity index 99% rename from packages/insomnia-app/app/network/o-auth-2/__tests__/helpers.js rename to packages/insomnia-app/app/network/o-auth-2/__tests__/helpers.ts index b09ab91a17..5a5cb7c7f4 100644 --- a/packages/insomnia-app/app/network/o-auth-2/__tests__/helpers.js +++ b/packages/insomnia-app/app/network/o-auth-2/__tests__/helpers.ts @@ -4,12 +4,17 @@ import EventEmitter from 'events'; export function createBWRedirectMock(redirectTo) { electron.remote.BrowserWindow = jest.fn(function() { this._emitter = new EventEmitter(); + this.loadURL = () => this.webContents.emit('did-navigate'); + this.on = (event, cb) => this._emitter.on(event, cb); + this.show = () => this._emitter.emit('show'); + this.close = () => this._emitter.emit('close'); this.webContents = new EventEmitter(); + this.webContents.getURL = () => redirectTo; this._emitter.emit('ready-to-show'); diff --git a/packages/insomnia-app/app/network/o-auth-2/__tests__/misc.test.js b/packages/insomnia-app/app/network/o-auth-2/__tests__/misc.test.ts similarity index 99% rename from packages/insomnia-app/app/network/o-auth-2/__tests__/misc.test.js rename to packages/insomnia-app/app/network/o-auth-2/__tests__/misc.test.ts index b6651d4838..e0716231bd 100644 --- a/packages/insomnia-app/app/network/o-auth-2/__tests__/misc.test.js +++ b/packages/insomnia-app/app/network/o-auth-2/__tests__/misc.test.ts @@ -9,9 +9,7 @@ describe('responseToObject()', () => { str: 'hi', num: 10, }); - const keys = ['str', 'num']; - expect(responseToObject(body, keys)).toEqual({ str: 'hi', num: 10, @@ -24,9 +22,7 @@ describe('responseToObject()', () => { num: 10, other: 'thing', }); - const keys = ['str']; - expect(responseToObject(body, keys)).toEqual({ str: 'hi', }); @@ -34,9 +30,7 @@ describe('responseToObject()', () => { it('works with things not found', () => { const body = JSON.stringify({}); - const keys = ['str']; - expect(responseToObject(body, keys)).toEqual({ str: null, }); @@ -47,14 +41,11 @@ describe('responseToObject()', () => { str: 'hi', num: 10, }); - const keys = ['str', 'missing']; - const defaults = { missing: 'found it!', str: 'should not see this', }; - expect(responseToObject(body, keys, defaults)).toEqual({ str: 'hi', missing: 'found it!', diff --git a/packages/insomnia-app/app/network/o-auth-2/constants.js b/packages/insomnia-app/app/network/o-auth-2/constants.ts similarity index 99% rename from packages/insomnia-app/app/network/o-auth-2/constants.js rename to packages/insomnia-app/app/network/o-auth-2/constants.ts index cb134fcb5b..807ce914c5 100644 --- a/packages/insomnia-app/app/network/o-auth-2/constants.js +++ b/packages/insomnia-app/app/network/o-auth-2/constants.ts @@ -3,12 +3,10 @@ export const GRANT_TYPE_IMPLICIT = 'implicit'; export const GRANT_TYPE_PASSWORD = 'password'; export const GRANT_TYPE_CLIENT_CREDENTIALS = 'client_credentials'; export const GRANT_TYPE_REFRESH = 'refresh_token'; - export const RESPONSE_TYPE_CODE = 'code'; export const RESPONSE_TYPE_ID_TOKEN = 'id_token'; export const RESPONSE_TYPE_TOKEN = 'token'; export const RESPONSE_TYPE_ID_TOKEN_TOKEN = 'id_token token'; - export const P_ACCESS_TOKEN = 'access_token'; export const P_ID_TOKEN = 'id_token'; export const P_CLIENT_ID = 'client_id'; @@ -33,6 +31,5 @@ export const P_SCOPE = 'scope'; export const P_STATE = 'state'; export const P_TOKEN_TYPE = 'token_type'; export const P_USERNAME = 'username'; - export const X_RESPONSE_ID = 'xResponseId'; export const X_ERROR = 'xError'; diff --git a/packages/insomnia-app/app/network/o-auth-2/get-token.js b/packages/insomnia-app/app/network/o-auth-2/get-token.ts similarity index 97% rename from packages/insomnia-app/app/network/o-auth-2/get-token.js rename to packages/insomnia-app/app/network/o-auth-2/get-token.ts index 8ff98f19d2..a5d945317a 100644 --- a/packages/insomnia-app/app/network/o-auth-2/get-token.js +++ b/packages/insomnia-app/app/network/o-auth-2/get-token.ts @@ -1,4 +1,3 @@ -// @flow import getAccessTokenAuthorizationCode from './grant-authorization-code'; import getAccessTokenClientCredentials from './grant-client-credentials'; import getAccessTokenPassword from './grant-password'; @@ -22,20 +21,23 @@ import { import * as models from '../../models'; import type { RequestAuthentication } from '../../models/request'; import type { OAuth2Token } from '../../models/o-auth-2-token'; - /** Get an OAuth2Token object and also handle storing/saving/refreshing */ + export default async function( requestId: string, authentication: RequestAuthentication, - forceRefresh: boolean = false, + forceRefresh = false, ): Promise { switch (authentication.grantType) { case GRANT_TYPE_AUTHORIZATION_CODE: return _getOAuth2AuthorizationCodeHeader(requestId, authentication, forceRefresh); + case GRANT_TYPE_CLIENT_CREDENTIALS: return _getOAuth2ClientCredentialsHeader(requestId, authentication, forceRefresh); + case GRANT_TYPE_IMPLICIT: return _getOAuth2ImplicitHeader(requestId, authentication, forceRefresh); + case GRANT_TYPE_PASSWORD: return _getOAuth2PasswordHeader(requestId, authentication, forceRefresh); } @@ -68,7 +70,6 @@ async function _getOAuth2AuthorizationCodeHeader( authentication.resource, authentication.usePkce, ); - return _updateOAuth2Token(requestId, results); } @@ -117,7 +118,6 @@ async function _getOAuth2ImplicitHeader( authentication.state, authentication.audience, ); - return _updateOAuth2Token(requestId, results); } @@ -143,7 +143,6 @@ async function _getOAuth2PasswordHeader( authentication.scope, authentication.audience, ); - return _updateOAuth2Token(requestId, results); } @@ -155,7 +154,6 @@ async function _getAccessToken( // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // See if we have a token already // // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // - const token: OAuth2Token | null = await models.oAuth2Token.getByParentId(requestId); if (!token) { @@ -165,7 +163,6 @@ async function _getAccessToken( // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // Check if the token needs refreshing // // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // - // Refresh tokens are part of Auth Code, Password const expiresAt = token.expiresAt || Infinity; const isExpired = Date.now() > expiresAt; @@ -177,7 +174,6 @@ async function _getAccessToken( // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // // Refresh the token if necessary // // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // - // We've expired, but don't have a refresh token, so tell caller to fetch new // access token if (!token.refreshToken) { @@ -203,17 +199,17 @@ async function _getAccessToken( // ~~~~~~~~~~~~~ // // Update the DB // // ~~~~~~~~~~~~~ // - return _updateOAuth2Token(requestId, refreshResults); } -async function _updateOAuth2Token(requestId: string, authResults: Object): Promise { +async function _updateOAuth2Token( + requestId: string, + authResults: Record, +): Promise { const oAuth2Token = await models.oAuth2Token.getOrCreateByParentId(requestId); - // Calculate expiry date const expiresIn = authResults[P_EXPIRES_IN]; const expiresAt = expiresIn ? Date.now() + expiresIn * 1000 : null; - return models.oAuth2Token.update(oAuth2Token, { expiresAt, refreshToken: authResults[P_REFRESH_TOKEN] || null, @@ -222,7 +218,6 @@ async function _updateOAuth2Token(requestId: string, authResults: Object): Promi error: authResults[P_ERROR] || null, errorDescription: authResults[P_ERROR_DESCRIPTION] || null, errorUri: authResults[P_ERROR_URI] || null, - // Special Cases xResponseId: authResults[X_RESPONSE_ID] || null, xError: authResults[X_ERROR] || null, diff --git a/packages/insomnia-app/app/network/o-auth-2/grant-authorization-code.js b/packages/insomnia-app/app/network/o-auth-2/grant-authorization-code.ts similarity index 62% rename from packages/insomnia-app/app/network/o-auth-2/grant-authorization-code.js rename to packages/insomnia-app/app/network/o-auth-2/grant-authorization-code.ts index 8af812041c..ed7f4571ff 100644 --- a/packages/insomnia-app/app/network/o-auth-2/grant-authorization-code.js +++ b/packages/insomnia-app/app/network/o-auth-2/grant-authorization-code.ts @@ -1,4 +1,3 @@ -// @flow import crypto from 'crypto'; import { parse as urlParse } from 'url'; import * as c from './constants'; @@ -16,13 +15,13 @@ export default async function( credentialsInBody: boolean, clientId: string, clientSecret: string, - redirectUri: string = '', - scope: string = '', - state: string = '', - audience: string = '', - resource: string = '', - usePkce: boolean = false, -): Promise { + redirectUri = '', + scope = '', + state = '', + audience = '', + resource = '', + usePkce = false, +): Promise> { if (!authorizeUrl) { throw new Error('Invalid authorization URL'); } @@ -35,13 +34,10 @@ export default async function( let codeChallenge = ''; if (usePkce) { + // @ts-expect-error -- TSCONVERSION codeVerifier = _base64UrlEncode(crypto.randomBytes(32)); - codeChallenge = _base64UrlEncode( - crypto - .createHash('sha256') - .update(codeVerifier) - .digest(), - ); + // @ts-expect-error -- TSCONVERSION + codeChallenge = _base64UrlEncode(crypto.createHash('sha256').update(codeVerifier).digest()); } const authorizeResults = await _authorize( @@ -89,20 +85,51 @@ async function _authorize( codeChallenge = '', ) { const params = [ - { name: c.P_RESPONSE_TYPE, value: c.RESPONSE_TYPE_CODE }, - { name: c.P_CLIENT_ID, value: clientId }, + { + name: c.P_RESPONSE_TYPE, + value: c.RESPONSE_TYPE_CODE, + }, + { + name: c.P_CLIENT_ID, + value: clientId, + }, ]; - // Add optional params - redirectUri && params.push({ name: c.P_REDIRECT_URI, value: redirectUri }); - scope && params.push({ name: c.P_SCOPE, value: scope }); - state && params.push({ name: c.P_STATE, value: state }); - audience && params.push({ name: c.P_AUDIENCE, value: audience }); - resource && params.push({ name: c.P_RESOURCE, value: resource }); + redirectUri && + params.push({ + name: c.P_REDIRECT_URI, + value: redirectUri, + }); + scope && + params.push({ + name: c.P_SCOPE, + value: scope, + }); + state && + params.push({ + name: c.P_STATE, + value: state, + }); + audience && + params.push({ + name: c.P_AUDIENCE, + value: audience, + }); + resource && + params.push({ + name: c.P_RESOURCE, + value: resource, + }); if (codeChallenge) { - params.push({ name: c.P_CODE_CHALLENGE, value: codeChallenge }); - params.push({ name: c.P_CODE_CHALLENGE_METHOD, value: 'S256' }); + params.push({ + name: c.P_CODE_CHALLENGE, + value: codeChallenge, + }); + params.push({ + name: c.P_CODE_CHALLENGE_METHOD, + value: 'S256', + }); } // Add query params to URL @@ -110,11 +137,8 @@ async function _authorize( const finalUrl = joinUrlAndQueryString(url, qs); const successRegex = new RegExp(`${escapeRegex(redirectUri)}.*(code=)`, 'i'); const failureRegex = new RegExp(`${escapeRegex(redirectUri)}.*(error=)`, 'i'); - const redirectedTo = await authorizeUserInWindow(finalUrl, successRegex, failureRegex); - console.log('[oauth2] Detected redirect ' + redirectedTo); - const { query } = urlParse(redirectedTo); return responseToObject(query, [ c.P_CODE, @@ -132,26 +156,53 @@ async function _getToken( clientId: string, clientSecret: string, code: string, - redirectUri: string = '', - state: string = '', - audience: string = '', - resource: string = '', - codeVerifier: string = '', -): Promise { + redirectUri = '', + state = '', + audience = '', + resource = '', + codeVerifier = '', +): Promise> { const params = [ - { name: c.P_GRANT_TYPE, value: c.GRANT_TYPE_AUTHORIZATION_CODE }, - { name: c.P_CODE, value: code }, + { + name: c.P_GRANT_TYPE, + value: c.GRANT_TYPE_AUTHORIZATION_CODE, + }, + { + name: c.P_CODE, + value: code, + }, ]; - // Add optional params - redirectUri && params.push({ name: c.P_REDIRECT_URI, value: redirectUri }); - state && params.push({ name: c.P_STATE, value: state }); - audience && params.push({ name: c.P_AUDIENCE, value: audience }); - resource && params.push({ name: c.P_RESOURCE, value: resource }); - codeVerifier && params.push({ name: c.P_CODE_VERIFIER, value: codeVerifier }); - + redirectUri && + params.push({ + name: c.P_REDIRECT_URI, + value: redirectUri, + }); + state && + params.push({ + name: c.P_STATE, + value: state, + }); + audience && + params.push({ + name: c.P_AUDIENCE, + value: audience, + }); + resource && + params.push({ + name: c.P_RESOURCE, + value: resource, + }); + codeVerifier && + params.push({ + name: c.P_CODE_VERIFIER, + value: codeVerifier, + }); const headers = [ - { name: 'Content-Type', value: 'application/x-www-form-urlencoded' }, + { + name: 'Content-Type', + value: 'application/x-www-form-urlencoded', + }, { name: 'Accept', value: 'application/x-www-form-urlencoded, application/json', @@ -159,8 +210,14 @@ async function _getToken( ]; if (credentialsInBody) { - params.push({ name: c.P_CLIENT_ID, value: clientId }); - params.push({ name: c.P_CLIENT_SECRET, value: clientSecret }); + params.push({ + name: c.P_CLIENT_ID, + value: clientId, + }); + params.push({ + name: c.P_CLIENT_SECRET, + value: clientSecret, + }); } else { headers.push(getBasicAuthHeader(clientId, clientSecret)); } @@ -171,10 +228,10 @@ async function _getToken( method: 'POST', body: models.request.newBodyFormUrlEncoded(params), }); - const response = await models.response.create(responsePatch); - + // @ts-expect-error -- TSCONVERSION const bodyBuffer = models.response.getBodyBuffer(response); + if (!bodyBuffer) { return { [c.X_ERROR]: `No body returned from ${url}`, @@ -182,7 +239,9 @@ async function _getToken( }; } + // @ts-expect-error -- TSCONVERSION const statusCode = response.statusCode || 0; + if (statusCode < 200 || statusCode >= 300) { return { [c.X_ERROR]: `Failed to fetch token url=${url} status=${statusCode}`, @@ -203,20 +262,17 @@ async function _getToken( c.P_ERROR_URI, c.P_ERROR_DESCRIPTION, ]); - results[c.X_RESPONSE_ID] = response._id; - return results; } -function _base64UrlEncode(str: string): string { - return str - .toString('base64') +function _base64UrlEncode(str: string) { + // @ts-expect-error -- TSCONVERSION appears to be genuine + return str.toString('base64') + // The characters + / = are reserved for PKCE as per the RFC, + // so we replace them with unreserved characters + // Docs: https://tools.ietf.org/html/rfc7636#section-4.2 .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, ''); - - // The characters + / = are reserved for PKCE as per the RFC, - // so we replace them with unreserved characters - // Docs: https://tools.ietf.org/html/rfc7636#section-4.2 } diff --git a/packages/insomnia-app/app/network/o-auth-2/grant-client-credentials.js b/packages/insomnia-app/app/network/o-auth-2/grant-client-credentials.ts similarity index 69% rename from packages/insomnia-app/app/network/o-auth-2/grant-client-credentials.js rename to packages/insomnia-app/app/network/o-auth-2/grant-client-credentials.ts index c4e68ca691..550ac96452 100644 --- a/packages/insomnia-app/app/network/o-auth-2/grant-client-credentials.js +++ b/packages/insomnia-app/app/network/o-auth-2/grant-client-credentials.ts @@ -1,4 +1,3 @@ -// @flow import { setDefaultProtocol } from 'insomnia-url'; import * as c from './constants'; import { responseToObject } from './misc'; @@ -12,19 +11,37 @@ export default async function( credentialsInBody: boolean, clientId: string, clientSecret: string, - scope: string = '', - audience: string = '', - resource: string = '', -): Promise { - const params = [{ name: c.P_GRANT_TYPE, value: c.GRANT_TYPE_CLIENT_CREDENTIALS }]; - + scope = '', + audience = '', + resource = '', +): Promise> { + const params = [ + { + name: c.P_GRANT_TYPE, + value: c.GRANT_TYPE_CLIENT_CREDENTIALS, + }, + ]; // Add optional params - scope && params.push({ name: c.P_SCOPE, value: scope }); - audience && params.push({ name: c.P_AUDIENCE, value: audience }); - resource && params.push({ name: c.P_RESOURCE, value: resource }); - + scope && + params.push({ + name: c.P_SCOPE, + value: scope, + }); + audience && + params.push({ + name: c.P_AUDIENCE, + value: audience, + }); + resource && + params.push({ + name: c.P_RESOURCE, + value: resource, + }); const headers = [ - { name: 'Content-Type', value: 'application/x-www-form-urlencoded' }, + { + name: 'Content-Type', + value: 'application/x-www-form-urlencoded', + }, { name: 'Accept', value: 'application/x-www-form-urlencoded, application/json', @@ -32,24 +49,29 @@ export default async function( ]; if (credentialsInBody) { - params.push({ name: c.P_CLIENT_ID, value: clientId }); - params.push({ name: c.P_CLIENT_SECRET, value: clientSecret }); + params.push({ + name: c.P_CLIENT_ID, + value: clientId, + }); + params.push({ + name: c.P_CLIENT_SECRET, + value: clientSecret, + }); } else { headers.push(getBasicAuthHeader(clientId, clientSecret)); } const url = setDefaultProtocol(accessTokenUrl); - const responsePatch = await sendWithSettings(requestId, { headers, url, method: 'POST', body: models.request.newBodyFormUrlEncoded(params), }); - const response = await models.response.create(responsePatch); - + // @ts-expect-error -- TSCONVERSION const bodyBuffer = models.response.getBodyBuffer(response); + if (!bodyBuffer) { return { [c.X_ERROR]: `No body returned from ${url}`, @@ -57,7 +79,9 @@ export default async function( }; } + // @ts-expect-error -- TSCONVERSION const statusCode = response.statusCode || 0; + if (statusCode < 200 || statusCode >= 300) { return { [c.X_ERROR]: `Failed to fetch token url=${url} status=${statusCode}`, @@ -77,8 +101,6 @@ export default async function( c.P_ERROR_URI, c.P_ERROR_DESCRIPTION, ]); - results[c.X_RESPONSE_ID] = response._id; - return results; } diff --git a/packages/insomnia-app/app/network/o-auth-2/grant-implicit.js b/packages/insomnia-app/app/network/o-auth-2/grant-implicit.ts similarity index 64% rename from packages/insomnia-app/app/network/o-auth-2/grant-implicit.js rename to packages/insomnia-app/app/network/o-auth-2/grant-implicit.ts index adbd4eba25..e596fc91df 100644 --- a/packages/insomnia-app/app/network/o-auth-2/grant-implicit.js +++ b/packages/insomnia-app/app/network/o-auth-2/grant-implicit.ts @@ -1,21 +1,26 @@ -// @flow import * as c from './constants'; import { buildQueryStringFromParams, joinUrlAndQueryString } from 'insomnia-url'; import { responseToObject, authorizeUserInWindow } from './misc'; export default async function( - requestId: string, + _requestId: string, authorizationUrl: string, clientId: string, responseType: string = c.RESPONSE_TYPE_TOKEN, - redirectUri: string = '', - scope: string = '', - state: string = '', - audience: string = '', -): Promise { + redirectUri = '', + scope = '', + state = '', + audience = '', +): Promise> { const params = [ - { name: c.P_RESPONSE_TYPE, value: responseType }, - { name: c.P_CLIENT_ID, value: clientId }, + { + name: c.P_RESPONSE_TYPE, + value: responseType, + }, + { + name: c.P_CLIENT_ID, + value: clientId, + }, ]; // Add optional params @@ -24,18 +29,36 @@ export default async function( responseType === c.RESPONSE_TYPE_ID_TOKEN ) { const nonce = Math.floor(Math.random() * 9999999999999) + 1; - params.push({ name: c.P_NONCE, value: nonce }); + params.push({ + name: c.P_NONCE, + // @ts-expect-error -- TSCONVERSION + value: nonce, + }); } - redirectUri && params.push({ name: c.P_REDIRECT_URI, value: redirectUri }); - scope && params.push({ name: c.P_SCOPE, value: scope }); - state && params.push({ name: c.P_STATE, value: state }); - audience && params.push({ name: c.P_AUDIENCE, value: audience }); - + redirectUri && + params.push({ + name: c.P_REDIRECT_URI, + value: redirectUri, + }); + scope && + params.push({ + name: c.P_SCOPE, + value: scope, + }); + state && + params.push({ + name: c.P_STATE, + value: state, + }); + audience && + params.push({ + name: c.P_AUDIENCE, + value: audience, + }); // Add query params to URL const qs = buildQueryStringFromParams(params); const finalUrl = joinUrlAndQueryString(authorizationUrl, qs); - const redirectedTo = await authorizeUserInWindow( finalUrl, /(access_token=|id_token=)/, diff --git a/packages/insomnia-app/app/network/o-auth-2/grant-password.js b/packages/insomnia-app/app/network/o-auth-2/grant-password.ts similarity index 70% rename from packages/insomnia-app/app/network/o-auth-2/grant-password.js rename to packages/insomnia-app/app/network/o-auth-2/grant-password.ts index dfd4635ffb..45aa562fb5 100644 --- a/packages/insomnia-app/app/network/o-auth-2/grant-password.js +++ b/packages/insomnia-app/app/network/o-auth-2/grant-password.ts @@ -1,4 +1,3 @@ -// @flow import { setDefaultProtocol } from 'insomnia-url'; import * as c from './constants'; import { responseToObject } from './misc'; @@ -14,21 +13,39 @@ export default async function( clientSecret: string, username: string, password: string, - scope: string = '', - audience: string = '', -): Promise { + scope = '', + audience = '', +): Promise> { const params = [ - { name: c.P_GRANT_TYPE, value: c.GRANT_TYPE_PASSWORD }, - { name: c.P_USERNAME, value: username }, - { name: c.P_PASSWORD, value: password }, + { + name: c.P_GRANT_TYPE, + value: c.GRANT_TYPE_PASSWORD, + }, + { + name: c.P_USERNAME, + value: username, + }, + { + name: c.P_PASSWORD, + value: password, + }, ]; - // Add optional params - scope && params.push({ name: c.P_SCOPE, value: scope }); - audience && params.push({ name: c.P_AUDIENCE, value: audience }); - + scope && + params.push({ + name: c.P_SCOPE, + value: scope, + }); + audience && + params.push({ + name: c.P_AUDIENCE, + value: audience, + }); const headers = [ - { name: 'Content-Type', value: 'application/x-www-form-urlencoded' }, + { + name: 'Content-Type', + value: 'application/x-www-form-urlencoded', + }, { name: 'Accept', value: 'application/x-www-form-urlencoded, application/json', @@ -36,24 +53,29 @@ export default async function( ]; if (credentialsInBody) { - params.push({ name: c.P_CLIENT_ID, value: clientId }); - params.push({ name: c.P_CLIENT_SECRET, value: clientSecret }); + params.push({ + name: c.P_CLIENT_ID, + value: clientId, + }); + params.push({ + name: c.P_CLIENT_SECRET, + value: clientSecret, + }); } else { headers.push(getBasicAuthHeader(clientId, clientSecret)); } const url = setDefaultProtocol(accessTokenUrl); - const responsePatch = await network.sendWithSettings(requestId, { url, headers, method: 'POST', body: models.request.newBodyFormUrlEncoded(params), }); - const response = await models.response.create(responsePatch); - + // @ts-expect-error -- TSCONVERSION const bodyBuffer = models.response.getBodyBuffer(response); + if (!bodyBuffer) { return { [c.X_ERROR]: `No body returned from ${url}`, @@ -61,7 +83,9 @@ export default async function( }; } + // @ts-expect-error -- TSCONVERSION const statusCode = response.statusCode || 0; + if (statusCode < 200 || statusCode >= 300) { return { [c.X_ERROR]: `Failed to fetch token url=${url} status=${statusCode}`, @@ -81,8 +105,6 @@ export default async function( c.P_ERROR_URI, c.P_ERROR_DESCRIPTION, ]); - results[c.X_RESPONSE_ID] = response._id; - return results; } diff --git a/packages/insomnia-app/app/network/o-auth-2/misc.js b/packages/insomnia-app/app/network/o-auth-2/misc.ts similarity index 91% rename from packages/insomnia-app/app/network/o-auth-2/misc.js rename to packages/insomnia-app/app/network/o-auth-2/misc.ts index 87d87b299d..61d6d93d52 100644 --- a/packages/insomnia-app/app/network/o-auth-2/misc.js +++ b/packages/insomnia-app/app/network/o-auth-2/misc.ts @@ -4,6 +4,7 @@ import querystring from 'querystring'; const LOCALSTORAGE_KEY_SESSION_ID = 'insomnia::current-oauth-session-id'; let authWindowSessionId; + if (window.localStorage.getItem(LOCALSTORAGE_KEY_SESSION_ID)) { authWindowSessionId = window.localStorage.getItem(LOCALSTORAGE_KEY_SESSION_ID); } else { @@ -18,7 +19,8 @@ export function initNewOAuthSession() { } export function responseToObject(body, keys, defaults = {}) { - let data = null; + let data: querystring.ParsedUrlQuery | null = null; + try { data = JSON.parse(body); } catch (err) {} @@ -37,6 +39,7 @@ export function responseToObject(body, keys, defaults = {}) { } const results = {}; + for (const key of keys) { if (data[key] !== undefined) { results[key] = data[key]; @@ -55,10 +58,10 @@ export function authorizeUserInWindow( urlSuccessRegex = /(code=).*/, urlFailureRegex = /(error=).*/, ) { - return new Promise((resolve, reject) => { - let finalUrl = null; + return new Promise((resolve, reject) => { + let finalUrl: string | null = null; - function _parseUrl(currentUrl, source) { + function _parseUrl(currentUrl: string, source: string) { if (currentUrl.match(urlSuccessRegex)) { console.log( `[oauth2] ${source}: Matched success redirect to "${currentUrl}" with ${urlSuccessRegex.toString()}`, @@ -89,7 +92,6 @@ export function authorizeUserInWindow( }, show: false, }); - // Finish on close child.on('close', () => { if (finalUrl) { @@ -99,26 +101,23 @@ export function authorizeUserInWindow( reject(new Error(errorDescription)); } }); - // Catch the redirect after login child.webContents.on('did-navigate', () => { // Be sure to resolve URL so that we can handle redirects with no host like /foo/bar const currentUrl = child.webContents.getURL(); + _parseUrl(currentUrl, 'did-navigate'); }); - - child.webContents.on('will-redirect', (e, url) => { + child.webContents.on('will-redirect', (_error, url) => { // Also listen for will-redirect, as some redirections do not trigger 'did-navigate' // 'will-redirect' does not cover all cases that 'did-navigate' does, so both events are required // GitHub's flow triggers only 'did-navigate', while Microsoft's only 'will-redirect' _parseUrl(url, 'will-redirect'); }); - - child.webContents.on('did-fail-load', (e, errorCode, errorDescription, url) => { + child.webContents.on('did-fail-load', (_error, _errorCode, _errorDescription, url) => { // Listen for did-fail-load to be able to parse the URL even when the callback server is unreachable _parseUrl(url, 'did-fail-load'); }); - // Show the window to the user after it loads child.on('ready-to-show', child.show.bind(child)); child.loadURL(url); diff --git a/packages/insomnia-app/app/network/o-auth-2/refresh-token.js b/packages/insomnia-app/app/network/o-auth-2/refresh-token.ts similarity index 82% rename from packages/insomnia-app/app/network/o-auth-2/refresh-token.js rename to packages/insomnia-app/app/network/o-auth-2/refresh-token.ts index 989fd245de..812c0d4005 100644 --- a/packages/insomnia-app/app/network/o-auth-2/refresh-token.js +++ b/packages/insomnia-app/app/network/o-auth-2/refresh-token.ts @@ -1,4 +1,3 @@ -// @flow import * as c from './constants'; import { responseToObject } from './misc'; import { setDefaultProtocol } from 'insomnia-url'; @@ -14,17 +13,28 @@ export default async function( clientSecret: string, refreshToken: string, scope: string, -): Promise { +): Promise> { const params = [ - { name: c.P_GRANT_TYPE, value: c.GRANT_TYPE_REFRESH }, - { name: c.P_REFRESH_TOKEN, value: refreshToken }, + { + name: c.P_GRANT_TYPE, + value: c.GRANT_TYPE_REFRESH, + }, + { + name: c.P_REFRESH_TOKEN, + value: refreshToken, + }, ]; - // Add optional params - scope && params.push({ name: c.P_SCOPE, value: scope }); - + scope && + params.push({ + name: c.P_SCOPE, + value: scope, + }); const headers = [ - { name: 'Content-Type', value: 'application/x-www-form-urlencoded' }, + { + name: 'Content-Type', + value: 'application/x-www-form-urlencoded', + }, { name: 'Accept', value: 'application/x-www-form-urlencoded, application/json', @@ -32,21 +42,25 @@ export default async function( ]; if (credentialsInBody) { - params.push({ name: c.P_CLIENT_ID, value: clientId }); - params.push({ name: c.P_CLIENT_SECRET, value: clientSecret }); + params.push({ + name: c.P_CLIENT_ID, + value: clientId, + }); + params.push({ + name: c.P_CLIENT_SECRET, + value: clientSecret, + }); } else { headers.push(getBasicAuthHeader(clientId, clientSecret)); } const url = setDefaultProtocol(accessTokenUrl); - const response = await sendWithSettings(requestId, { headers, url, method: 'POST', body: models.request.newBodyFormUrlEncoded(params), }); - const statusCode = response.statusCode || 0; const bodyBuffer = models.response.getBodyBuffer(response); @@ -54,7 +68,6 @@ export default async function( // If the refresh token was rejected due an unauthorized request, we will // return a null access_token to trigger an authentication request to fetch // brand new refresh and access tokens. - return responseToObject(null, [c.P_ACCESS_TOKEN]); } else if (statusCode < 200 || statusCode >= 300) { if (bodyBuffer && statusCode === 400) { @@ -63,7 +76,6 @@ export default async function( // If the refresh token was rejected due an oauth2 invalid_grant error, we will // return a null access_token to trigger an authentication request to fetch // brand new refresh and access tokens. - if (response[c.P_ERROR] === 'invalid_grant') { return responseToObject(null, [c.P_ACCESS_TOKEN]); } diff --git a/packages/insomnia-app/app/network/url-matches-cert-host.js b/packages/insomnia-app/app/network/url-matches-cert-host.ts similarity index 88% rename from packages/insomnia-app/app/network/url-matches-cert-host.js rename to packages/insomnia-app/app/network/url-matches-cert-host.ts index 74f1b30780..d413abf611 100644 --- a/packages/insomnia-app/app/network/url-matches-cert-host.js +++ b/packages/insomnia-app/app/network/url-matches-cert-host.ts @@ -9,10 +9,10 @@ export function urlMatchesCertHost(certificateHost, requestUrl) { const cHostWithProtocol = setDefaultProtocol(certificateHost, 'https:'); const { hostname, port } = urlParse(requestUrl); const { hostname: cHostname, port: cPort } = certificateUrlParse(cHostWithProtocol); - + // @ts-expect-error -- TSCONVERSION `parseInt(null)` returns `NaN` const assumedPort = parseInt(port) || DEFAULT_PORT; + // @ts-expect-error -- TSCONVERSION `parseInt(null)` returns `NaN` const assumedCPort = parseInt(cPort) || DEFAULT_PORT; - const cHostnameRegex = escapeRegex(cHostname || '').replace(/\\\*/g, '.*'); const cPortRegex = escapeRegex(cPort || '').replace(/\\\*/g, '.*'); diff --git a/packages/insomnia-app/app/plugins/context/__tests__/app.test.js b/packages/insomnia-app/app/plugins/context/__tests__/app.test.ts similarity index 91% rename from packages/insomnia-app/app/plugins/context/__tests__/app.test.js rename to packages/insomnia-app/app/plugins/context/__tests__/app.test.ts index cc51a26b56..ef3e463c2a 100644 --- a/packages/insomnia-app/app/plugins/context/__tests__/app.test.js +++ b/packages/insomnia-app/app/plugins/context/__tests__/app.test.ts @@ -5,6 +5,7 @@ import { RENDER_PURPOSE_SEND } from '../../../common/render'; describe('init()', () => { beforeEach(globalBeforeEach); + it('initializes correctly', async () => { const result = plugin.init(); expect(Object.keys(result)).toEqual(['app', '__private']); @@ -21,12 +22,11 @@ describe('init()', () => { describe('app.alert()', () => { beforeEach(globalBeforeEach); + it('does not show alert when not sending', async () => { modals.showAlert = jest.fn(); const result = plugin.init(); - result.app.alert(); - // Make sure it passes correct arguments expect(modals.showAlert.mock.calls).toEqual([]); }); @@ -34,27 +34,33 @@ describe('app.alert()', () => { it('shows alert with message when sending', async () => { modals.showAlert = jest.fn().mockReturnValue('dummy-return-value'); const result = plugin.init(RENDER_PURPOSE_SEND); - // Make sure it returns result of showAlert() expect(result.app.alert('Title')).toBe('dummy-return-value'); expect(result.app.alert('Title', 'Message')).toBe('dummy-return-value'); - // Make sure it passes correct arguments expect(modals.showAlert.mock.calls).toEqual([ - [{ title: 'Title' }], - [{ title: 'Title', message: 'Message' }], + [ + { + title: 'Title', + }, + ], + [ + { + title: 'Title', + message: 'Message', + }, + ], ]); }); }); describe('app.prompt()', () => { beforeEach(globalBeforeEach); + it('does not show prompt when not sending', async () => { modals.showPrompt = jest.fn(); const result = plugin.init(); - result.app.prompt(); - // Make sure it passes correct arguments expect(modals.showPrompt.mock.calls).toEqual([]); }); @@ -62,11 +68,11 @@ describe('app.prompt()', () => { it('shows alert with message when sending', async () => { modals.showPrompt = jest.fn(); const result = plugin.init(RENDER_PURPOSE_SEND); - // Make sure it returns result of showAlert() result.app.prompt('Title'); - result.app.prompt('Title', { label: 'Label' }); - + result.app.prompt('Title', { + label: 'Label', + }); // Make sure it passes correct arguments expect(modals.showPrompt.mock.calls).toEqual([ [ diff --git a/packages/insomnia-app/app/plugins/context/__tests__/data.test.js b/packages/insomnia-app/app/plugins/context/__tests__/data.test.ts similarity index 90% rename from packages/insomnia-app/app/plugins/context/__tests__/data.test.js rename to packages/insomnia-app/app/plugins/context/__tests__/data.test.ts index ac1e1b72b8..da264a37a1 100644 --- a/packages/insomnia-app/app/plugins/context/__tests__/data.test.js +++ b/packages/insomnia-app/app/plugins/context/__tests__/data.test.ts @@ -3,7 +3,7 @@ import * as modals from '../../../ui/components/modals'; import path from 'path'; import { globalBeforeEach } from '../../../__jest__/before-each'; import * as models from '../../../models/index'; -import * as db from '../../../common/database'; +import { database as db } from '../../../common/database'; import fs from 'fs'; import { getAppVersion } from '../../../common/constants'; import { WorkspaceScopeKeys } from '../../../models/workspace'; @@ -17,8 +17,12 @@ const PLUGIN = { describe('init()', () => { beforeEach(globalBeforeEach); + it('initializes correctly', async () => { - const { data } = plugin.init({ name: PLUGIN }); + // @ts-expect-error -- TSCONVERSION genuine, plugin.init doesn't take any arguments + const { data } = plugin.init({ + name: PLUGIN, + }); expect(Object.keys(data)).toEqual(['import', 'export']); expect(Object.keys(data.export).sort()).toEqual(['har', 'insomnia']); expect(Object.keys(data.import).sort()).toEqual(['raw', 'uri']); @@ -34,16 +38,17 @@ describe('app.import.*', () => { modified: 222, }); }); + it('uri', async () => { + // @ts-expect-error -- TSCONVERSION mocking with jest function modals.showModal = jest.fn(); const workspace = await models.workspace.getById('wrk_1'); expect(await db.all(models.workspace.type)).toEqual([workspace]); expect(await db.count(models.request.type)).toBe(0); - + // @ts-expect-error -- TSCONVERSION genuine, plugin.init doesn't take any arguments const { data } = plugin.init(PLUGIN); const filename = path.resolve(__dirname, '../__fixtures__/basic-import.json'); await data.import.uri(`file://${filename}`); - const allWorkspaces = await db.all(models.workspace.type); expect(allWorkspaces).toEqual([ workspace, @@ -86,15 +91,15 @@ describe('app.import.*', () => { }); it('importRaw', async () => { + // @ts-expect-error -- TSCONVERSION mocking with jest function modals.showModal = jest.fn(); const workspace = await models.workspace.getById('wrk_1'); expect(await db.all(models.workspace.type)).toEqual([workspace]); expect(await db.count(models.request.type)).toBe(0); - + // @ts-expect-error -- TSCONVERSION genuine, plugin.init doesn't take any arguments const { data } = plugin.init(PLUGIN); const filename = path.resolve(__dirname, '../__fixtures__/basic-import.json'); await data.import.raw(fs.readFileSync(filename, 'utf8')); - const allWorkspaces = await db.all(models.workspace.type); expect(allWorkspaces).toEqual([ workspace, @@ -162,8 +167,9 @@ describe('app.export.*', () => { }); it('insomnia', async () => { + // @ts-expect-error -- TSCONVERSION mocking with jest function modals.showModal = jest.fn(); - + // @ts-expect-error -- TSCONVERSION genuine, plugin.init doesn't take any arguments const { data } = plugin.init(PLUGIN); const exported = await data.export.insomnia(); const exportedData = JSON.parse(exported); @@ -213,8 +219,9 @@ describe('app.export.*', () => { }); it('har', async () => { + // @ts-expect-error -- TSCONVERSION mocking with jest function modals.showModal = jest.fn(); - + // @ts-expect-error -- TSCONVERSION genuine, plugin.init doesn't take any arguments const { data } = plugin.init(PLUGIN); const exported = await data.export.har(); const exportedData = JSON.parse(exported); diff --git a/packages/insomnia-app/app/plugins/context/__tests__/request.test.js b/packages/insomnia-app/app/plugins/context/__tests__/request.test.ts similarity index 84% rename from packages/insomnia-app/app/plugins/context/__tests__/request.test.js rename to packages/insomnia-app/app/plugins/context/__tests__/request.test.ts index 9849d70c53..c984cee2e8 100644 --- a/packages/insomnia-app/app/plugins/context/__tests__/request.test.js +++ b/packages/insomnia-app/app/plugins/context/__tests__/request.test.ts @@ -7,14 +7,20 @@ const CONTEXT = { user_key: 'my_user_key', hello: 'world', array_test: ['a', 'b'], - object_test: { a: 'A', b: 'B' }, + object_test: { + a: 'A', + b: 'B', + }, null_test: null, }; describe('init()', () => { beforeEach(async () => { await globalBeforeEach(); - await models.workspace.create({ _id: 'wrk_1', name: 'My Workspace' }); + await models.workspace.create({ + _id: 'wrk_1', + name: 'My Workspace', + }); await models.request.create({ _id: 'req_1', parentId: 'wrk_1', @@ -91,27 +97,45 @@ describe('init()', () => { describe('request.*', () => { beforeEach(async () => { await globalBeforeEach(); - await models.workspace.create({ _id: 'wrk_1', name: 'My Workspace' }); + await models.workspace.create({ + _id: 'wrk_1', + name: 'My Workspace', + }); await models.request.create({ _id: 'req_1', parentId: 'wrk_1', name: 'My Request', - body: { text: 'body' }, - authentication: { type: 'oauth2' }, + body: { + text: 'body', + }, + authentication: { + type: 'oauth2', + }, headers: [ - { name: 'hello', value: 'world' }, - { name: 'Content-Type', value: 'application/json' }, + { + name: 'hello', + value: 'world', + }, + { + name: 'Content-Type', + value: 'application/json', + }, ], parameters: [ - { name: 'foo', value: 'bar' }, - { name: 'message', value: 'Hello World!' }, + { + name: 'foo', + value: 'bar', + }, + { + name: 'message', + value: 'Hello World!', + }, ], }); }); it('works for basic getters', async () => { const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {}); - const result = plugin.init(await models.request.getById('req_1'), CONTEXT); expect(result.request.getId()).toBe('req_1'); expect(result.request.getName()).toBe('My Request'); @@ -121,34 +145,37 @@ describe('request.*', () => { expect(consoleWarnSpy).toHaveBeenCalledWith( 'request.getBodyText() is deprecated. Use request.getBody() instead.', ); - expect(result.request.getAuthentication()).toEqual({ type: 'oauth2' }); + expect(result.request.getAuthentication()).toEqual({ + type: 'oauth2', + }); }); it('works for parameters', async () => { const result = plugin.init(await models.request.getById('req_1'), CONTEXT); - // getParameters() expect(result.request.getParameters()).toEqual([ - { name: 'foo', value: 'bar' }, - { name: 'message', value: 'Hello World!' }, + { + name: 'foo', + value: 'bar', + }, + { + name: 'message', + value: 'Hello World!', + }, ]); - // getParameter() expect(result.request.getParameter('foo')).toBe('bar'); expect(result.request.getParameter('FOO')).toBe(null); expect(result.request.getParameter('does-not-exist')).toBe(null); expect(result.request.hasParameter('foo')).toBe(true); - // setHeader() result.request.setParameter('foo', 'baz'); expect(result.request.getParameter('foo')).toBe('baz'); - // addHeader() result.request.addParameter('foo', 'another'); result.request.addParameter('something-else', 'yet another'); expect(result.request.getParameter('foo')).toBe('baz'); expect(result.request.getParameter('something-else')).toBe('yet another'); - // removeHeader() result.request.removeParameter('foo'); expect(result.request.getParameter('foo')).toBe(null); @@ -157,29 +184,30 @@ describe('request.*', () => { it('works for headers', async () => { const result = plugin.init(await models.request.getById('req_1'), CONTEXT); - // getHeaders() expect(result.request.getHeaders()).toEqual([ - { name: 'hello', value: 'world' }, - { name: 'Content-Type', value: 'application/json' }, + { + name: 'hello', + value: 'world', + }, + { + name: 'Content-Type', + value: 'application/json', + }, ]); - // getHeader() expect(result.request.getHeader('content-type')).toBe('application/json'); expect(result.request.getHeader('CONTENT-TYPE')).toBe('application/json'); expect(result.request.getHeader('does-not-exist')).toBe(null); expect(result.request.hasHeader('Content-Type')).toBe(true); - // setHeader() result.request.setHeader('content-type', 'text/plain'); expect(result.request.getHeader('Content-Type')).toBe('text/plain'); - // addHeader() result.request.addHeader('content-type', 'new/type'); result.request.addHeader('something-else', 'foo'); expect(result.request.getHeader('Content-Type')).toBe('text/plain'); expect(result.request.getHeader('something-else')).toBe('foo'); - // removeHeader() result.request.removeHeader('content-type'); expect(result.request.getHeader('Content-Type')).toBe(null); @@ -191,10 +219,14 @@ describe('request.*', () => { request.cookies = []; // Because the plugin technically needs a RenderedRequest const result = plugin.init(request, CONTEXT); - result.request.setCookie('foo', 'bar'); result.request.setCookie('foo', 'baz'); - expect(request.cookies).toEqual([{ name: 'foo', value: 'baz' }]); + expect(request.cookies).toEqual([ + { + name: 'foo', + value: 'baz', + }, + ]); }); it('works for environment', async () => { @@ -202,16 +234,17 @@ describe('request.*', () => { request.cookies = []; // Because the plugin technically needs a RenderedRequest const result = plugin.init(request, CONTEXT); - // getEnvironment expect(result.request.getEnvironment()).toEqual({ user_key: 'my_user_key', hello: 'world', array_test: ['a', 'b'], - object_test: { a: 'A', b: 'B' }, + object_test: { + a: 'A', + b: 'B', + }, null_test: null, }); - // getEnvironmentVariable expect(result.request.getEnvironmentVariable('user_key')).toBe('my_user_key'); expect(result.request.getEnvironmentVariable('hello')).toBe('world'); @@ -229,16 +262,21 @@ describe('request.*', () => { request.authentication = {}; // Because the plugin technically needs a RenderedRequest const result = plugin.init(request, CONTEXT); - result.request.setAuthenticationParameter('foo', 'bar'); result.request.setAuthenticationParameter('foo', 'baz'); - expect(result.request.getAuthentication()).toEqual({ foo: 'baz' }); - expect(request.authentication).toEqual({ foo: 'baz' }); + expect(result.request.getAuthentication()).toEqual({ + foo: 'baz', + }); + expect(request.authentication).toEqual({ + foo: 'baz', + }); }); it('works for request body', async () => { const result = plugin.init(await models.request.getById('req_1'), CONTEXT); - expect(result.request.getBody()).toEqual({ text: 'body' }); + expect(result.request.getBody()).toEqual({ + text: 'body', + }); const newBody = { mimeType: CONTENT_TYPE_FORM_URLENCODED, params: [ diff --git a/packages/insomnia-app/app/plugins/context/__tests__/response.test.js b/packages/insomnia-app/app/plugins/context/__tests__/response.test.ts similarity index 83% rename from packages/insomnia-app/app/plugins/context/__tests__/response.test.js rename to packages/insomnia-app/app/plugins/context/__tests__/response.test.ts index 745f163fe6..a4261c8359 100644 --- a/packages/insomnia-app/app/plugins/context/__tests__/response.test.js +++ b/packages/insomnia-app/app/plugins/context/__tests__/response.test.ts @@ -1,12 +1,13 @@ import * as plugin from '../response'; import { globalBeforeEach } from '../../../__jest__/before-each'; -import { getTempDir } from '../../../common/constants'; +import { getTempDir } from '../../../common/electron-helpers'; import fs from 'fs'; import path from 'path'; import * as models from '../../../models/index'; describe('init()', () => { beforeEach(globalBeforeEach); + it('initializes correctly', async () => { const result = plugin.init({}); expect(Object.keys(result)).toEqual(['response']); @@ -32,6 +33,7 @@ describe('init()', () => { describe('response.*', () => { beforeEach(globalBeforeEach); + it('works for basic and full response', async () => { const bodyPath = path.join(getTempDir(), 'response.zip'); fs.writeFileSync(bodyPath, Buffer.from('Hello World!')); @@ -65,16 +67,34 @@ describe('response.*', () => { it('works for getting headers', () => { const response = { headers: [ - { name: 'content-type', value: 'application/json' }, - { name: 'set-cookie', value: 'foo=bar' }, - { name: 'set-cookie', value: 'baz=qux' }, + { + name: 'content-type', + value: 'application/json', + }, + { + name: 'set-cookie', + value: 'foo=bar', + }, + { + name: 'set-cookie', + value: 'baz=qux', + }, ], }; const result = plugin.init(response); expect(result.response.getHeaders()).toEqual([ - { name: 'content-type', value: 'application/json' }, - { name: 'set-cookie', value: 'foo=bar' }, - { name: 'set-cookie', value: 'baz=qux' }, + { + name: 'content-type', + value: 'application/json', + }, + { + name: 'set-cookie', + value: 'foo=bar', + }, + { + name: 'set-cookie', + value: 'baz=qux', + }, ]); expect(result.response.getHeader('Does-Not-Exist')).toBeNull(); expect(result.response.getHeader('CONTENT-TYPE')).toBe('application/json'); diff --git a/packages/insomnia-app/app/plugins/context/__tests__/store.test.js b/packages/insomnia-app/app/plugins/context/__tests__/store.test.ts similarity index 86% rename from packages/insomnia-app/app/plugins/context/__tests__/store.test.js rename to packages/insomnia-app/app/plugins/context/__tests__/store.test.ts index 718cc1ccbf..b38b5d5918 100644 --- a/packages/insomnia-app/app/plugins/context/__tests__/store.test.js +++ b/packages/insomnia-app/app/plugins/context/__tests__/store.test.ts @@ -1,6 +1,5 @@ import * as plugin from '../store'; import { globalBeforeEach } from '../../../__jest__/before-each'; - const PLUGIN = { name: 'my-plugin', version: '1.0.0', @@ -10,8 +9,11 @@ const PLUGIN = { describe('init()', () => { beforeEach(globalBeforeEach); + it('initializes correctly', async () => { - const result = plugin.init({ name: PLUGIN }); + const result = plugin.init({ + name: PLUGIN, + }); expect(Object.keys(result.store).sort()).toEqual([ 'all', 'clear', @@ -25,33 +27,38 @@ describe('init()', () => { describe('store.*', () => { beforeEach(globalBeforeEach); + it('all methods work', async () => { const p = plugin.init(PLUGIN); - // Null item for no result expect(await p.store.getItem('unset-key')).toBeNull(); - // Add something await p.store.setItem('color', 'blue'); expect(await p.store.getItem('color')).toBe('blue'); expect(await p.store.hasItem('color')).toBe(true); - // Remove something await p.store.removeItem('color'); expect(await p.store.hasItem('color')).toBe(false); expect(await p.store.getItem('color')).toBeNull(); - // Add some more await p.store.setItem('a', 'aaa'); await p.store.setItem('b', 'bbb'); await p.store.setItem('c', 'ccc'); const all = await p.store.all(); expect(all.sort((a, b) => (a.key < b.key ? -1 : 1))).toEqual([ - { key: 'a', value: 'aaa' }, - { key: 'b', value: 'bbb' }, - { key: 'c', value: 'ccc' }, + { + key: 'a', + value: 'aaa', + }, + { + key: 'b', + value: 'bbb', + }, + { + key: 'c', + value: 'ccc', + }, ]); - // Clear it await p.store.clear(); expect(await p.store.getItem('a')).toBeNull(); diff --git a/packages/insomnia-app/app/plugins/context/app.js b/packages/insomnia-app/app/plugins/context/app.tsx similarity index 76% rename from packages/insomnia-app/app/plugins/context/app.js rename to packages/insomnia-app/app/plugins/context/app.tsx index 4fd97bc4e5..70391e40b5 100644 --- a/packages/insomnia-app/app/plugins/context/app.js +++ b/packages/insomnia-app/app/plugins/context/app.tsx @@ -1,5 +1,4 @@ -// @flow -import * as React from 'react'; +import React from 'react'; import * as electron from 'electron'; import { showAlert, showModal, showPrompt } from '../../ui/components/modals'; import type { RenderPurpose } from '../../common/render'; @@ -13,29 +12,34 @@ import HtmlElementWrapper from '../../ui/components/html-element-wrapper'; import { axiosRequest as axios } from '../../../app/network/axios-request'; import * as analytics from '../../../app/common/analytics'; -export function init(renderPurpose: RenderPurpose = RENDER_PURPOSE_GENERAL): { app: Object } { +export function init(renderPurpose: RenderPurpose = RENDER_PURPOSE_GENERAL): { + app: Record; +} { const canShowDialogs = renderPurpose === RENDER_PURPOSE_SEND || renderPurpose === RENDER_PURPOSE_NO_RENDER; - return { app: { - alert(title: string, message?: string): Promise { + alert(title: string, message?: string) { if (!canShowDialogs) { return Promise.resolve(); } - return showAlert({ title, message }); + return showAlert({ + title, + message, + }); }, + dialog( title, body: HTMLElement, - options?: { - onHide?: () => void, - tall?: boolean, - skinny?: boolean, - wide?: boolean, + options: { + onHide?: () => void; + tall?: boolean; + skinny?: boolean; + wide?: boolean; } = {}, - ): void { + ) { if (renderPurpose !== RENDER_PURPOSE_SEND && renderPurpose !== RENDER_PURPOSE_NO_RENDER) { return; } @@ -48,15 +52,16 @@ export function init(renderPurpose: RenderPurpose = RENDER_PURPOSE_GENERAL): { a wide: options.wide, }); }, + prompt( title: string, options?: { - label?: string, - defaultValue?: string, - submitName?: string, - cancelable?: boolean, + label?: string; + defaultValue?: string; + submitName?: string; + cancelable?: boolean; }, - ): Promise { + ) { options = options || {}; if (!canShowDialogs) { @@ -66,25 +71,34 @@ export function init(renderPurpose: RenderPurpose = RENDER_PURPOSE_GENERAL): { a return new Promise((resolve, reject) => { showPrompt({ title, - ...(options || {}: Object), + ...(options || ({} as Record)), + onCancel() { reject(new Error(`Prompt ${title} cancelled`)); }, + onComplete(value: string) { resolve(value); }, }); }); }, - getPath(name: string): string { + + getPath(name: string) { switch (name.toLowerCase()) { case 'desktop': return electron.remote.app.getPath('desktop'); + default: throw new Error(`Unknown path name ${name}`); } }, - async showSaveDialog(options: { defaultPath?: string } = {}): Promise { + + async showSaveDialog( + options: { + defaultPath?: string; + } = {}, + ): Promise { if (!canShowDialogs) { return Promise.resolve(null); } @@ -94,7 +108,6 @@ export function init(renderPurpose: RenderPurpose = RENDER_PURPOSE_GENERAL): { a buttonLabel: 'Save', defaultPath: options.defaultPath, }; - const { filePath } = await electron.remote.dialog.showSaveDialog(saveOptions); return filePath || null; }, @@ -104,16 +117,21 @@ export function init(renderPurpose: RenderPurpose = RENDER_PURPOSE_GENERAL): { a // ~~~~~~~~~~~~~~~~~~ // /** @deprecated as it was never officially supported */ - showGenericModalDialog(title: string, options?: { html: string } = {}): void { + showGenericModalDialog( + title: string, + options: { + html?: string; + } = {}, + ) { console.warn('app.showGenericModalDialog() is deprecated. Use app.dialog() instead.'); - // Create DOM node so we can adapt to the new dialog() method const body = document.createElement('div'); + // @ts-expect-error -- TSCONVERSION body.innerHTML = options.html; - return this.dialog(title, body); }, }, + // @ts-expect-error -- TSCONVERSION __private: { axios, analytics, diff --git a/packages/insomnia-app/app/plugins/context/data.js b/packages/insomnia-app/app/plugins/context/data.ts similarity index 66% rename from packages/insomnia-app/app/plugins/context/data.js rename to packages/insomnia-app/app/plugins/context/data.ts index 6900f5ebb3..4839b455a7 100644 --- a/packages/insomnia-app/app/plugins/context/data.js +++ b/packages/insomnia-app/app/plugins/context/data.ts @@ -1,4 +1,3 @@ -// @flow import { exportWorkspacesHAR, exportWorkspacesData, @@ -8,27 +7,30 @@ import { import type { Workspace, WorkspaceScope } from '../../models/workspace'; import type { ImportRawConfig } from '../../common/import'; -type PluginImportOptions = { workspaceId?: string, scope?: WorkspaceScope }; +interface PluginImportOptions { + workspaceId?: string; + scope?: WorkspaceScope; +} -export function init(): { data: { import: Object, export: Object } } { +export function init() { return { data: { import: { - async uri(uri: string, options: PluginImportOptions = {}): Promise { + async uri(uri: string, options: PluginImportOptions = {}) { await importUri(uri, buildImportRawConfig(options)); }, - async raw(text: string, options: PluginImportOptions = {}): Promise { + async raw(text: string, options: PluginImportOptions = {}) { await importRaw(text, buildImportRawConfig(options)); }, }, export: { async insomnia( options: { - includePrivate?: boolean, - format?: 'json' | 'yaml', - workspace?: Workspace, + includePrivate?: boolean; + format?: 'json' | 'yaml'; + workspace?: Workspace; } = {}, - ): Promise { + ) { options = options || {}; return exportWorkspacesData( options.workspace || null, @@ -36,9 +38,13 @@ export function init(): { data: { import: Object, export: Object } } { options.format || 'json', ); }, + async har( - options: { includePrivate?: boolean, workspace?: Workspace } = {}, - ): Promise { + options: { + includePrivate?: boolean; + workspace?: Workspace; + } = {}, + ) { return exportWorkspacesHAR(options.workspace || null, !!options.includePrivate); }, }, @@ -48,6 +54,13 @@ export function init(): { data: { import: Object, export: Object } } { function buildImportRawConfig(options: PluginImportOptions): ImportRawConfig { const getWorkspaceId = () => Promise.resolve(options.workspaceId || null); - const getWorkspaceScope = options.scope && (() => Promise.resolve(options.scope)); - return { getWorkspaceId, getWorkspaceScope }; + + const getWorkspaceScope = options.scope && (() => ( + Promise.resolve(options.scope as WorkspaceScope)) + ); + + return { + getWorkspaceId, + getWorkspaceScope, + }; } diff --git a/packages/insomnia-app/app/plugins/context/index.js b/packages/insomnia-app/app/plugins/context/index.ts similarity index 98% rename from packages/insomnia-app/app/plugins/context/index.js rename to packages/insomnia-app/app/plugins/context/index.ts index 36fa98f3c0..95c5592ba0 100644 --- a/packages/insomnia-app/app/plugins/context/index.js +++ b/packages/insomnia-app/app/plugins/context/index.ts @@ -1,4 +1,3 @@ -// @flow import * as _app from './app'; import * as _data from './data'; import * as _network from './network'; diff --git a/packages/insomnia-app/app/plugins/context/network.js b/packages/insomnia-app/app/plugins/context/network.js deleted file mode 100644 index c4b90c626f..0000000000 --- a/packages/insomnia-app/app/plugins/context/network.js +++ /dev/null @@ -1,18 +0,0 @@ -// @flow - -import { send } from '../../network/network'; -import type { Request } from '../../models/request'; -import * as models from '../../models'; -import type { ExtraRenderInfo } from '../../common/render'; - -export function init(activeEnvironmentId: string | null): { network: Object } { - const network = { - async sendRequest(request: Request, extraInfo?: ExtraRenderInfo): Promise { - const responsePatch = await send(request._id, activeEnvironmentId || undefined, extraInfo); - const settings = await models.settings.getOrCreate(); - return models.response.create(responsePatch, settings.maxHistoryResponses); - }, - }; - - return { network }; -} diff --git a/packages/insomnia-app/app/plugins/context/network.ts b/packages/insomnia-app/app/plugins/context/network.ts new file mode 100644 index 0000000000..57a1c6fb52 --- /dev/null +++ b/packages/insomnia-app/app/plugins/context/network.ts @@ -0,0 +1,16 @@ +import { send } from '../../network/network'; +import type { Request } from '../../models/request'; +import * as models from '../../models'; +import type { ExtraRenderInfo } from '../../common/render'; + +export function init(activeEnvironmentId: string | null) { + return { + network: { + async sendRequest(request: Request, extraInfo?: ExtraRenderInfo) { + const responsePatch = await send(request._id, activeEnvironmentId || undefined, extraInfo); + const settings = await models.settings.getOrCreate(); + return models.response.create(responsePatch, settings.maxHistoryResponses); + }, + }, + }; +} diff --git a/packages/insomnia-app/app/plugins/context/request.js b/packages/insomnia-app/app/plugins/context/request.ts similarity index 58% rename from packages/insomnia-app/app/plugins/context/request.js rename to packages/insomnia-app/app/plugins/context/request.ts index f0a641541a..a8e724fde4 100644 --- a/packages/insomnia-app/app/plugins/context/request.js +++ b/packages/insomnia-app/app/plugins/context/request.ts @@ -1,67 +1,87 @@ -// @flow import type { RenderedRequest } from '../../common/render'; import type { RequestBody } from '../../models/request'; import * as misc from '../../common/misc'; export function init( - renderedRequest: RenderedRequest, - renderedContext: Object, - readOnly: boolean = false, -): { request: Object } { + renderedRequest: RenderedRequest | null, + renderedContext: Record, + readOnly = false, +) { if (!renderedRequest) { throw new Error('contexts.request initialized without request'); } - const request = ({ - getId(): string { + const request = { + getId() { return renderedRequest._id; }, - getName(): string { + + getName() { return renderedRequest.name; }, - getUrl(): string { + + getUrl() { return renderedRequest.url; }, - getMethod(): string { + + getMethod() { return renderedRequest.method; }, - setMethod(method: string): void { + + setMethod(method: string) { renderedRequest.method = method; }, - setUrl(url: string): void { + + setUrl(url: string) { renderedRequest.url = url; }, - setCookie(name: string, value: string): void { + + setCookie(name: string, value: string) { const cookie = renderedRequest.cookies.find(c => c.name === name); + if (cookie) { cookie.value = value; } else { - renderedRequest.cookies.push({ name, value }); + renderedRequest.cookies.push({ + name, + value, + }); } }, - getEnvironmentVariable(name: string): string | number | boolean | Object | Array | null { + + getEnvironmentVariable( + name: string, + ): string | number | boolean | Record | Array | null { return renderedContext[name]; }, - getEnvironment(): Object { + + getEnvironment() { return renderedContext; }, + settingSendCookies(enabled: boolean) { renderedRequest.settingSendCookies = enabled; }, + settingStoreCookies(enabled: boolean) { renderedRequest.settingStoreCookies = enabled; }, + settingEncodeUrl(enabled: boolean) { renderedRequest.settingEncodeUrl = enabled; }, + settingDisableRenderRequestBody(enabled: boolean) { renderedRequest.settingDisableRenderRequestBody = enabled; }, + settingFollowRedirects(enabled: string) { renderedRequest.settingFollowRedirects = enabled; }, - getHeader(name: string): string | null { + + getHeader(name: string) { const headers = misc.filterHeaders(renderedRequest.headers, name); + if (headers.length) { // Use the last header if there are multiple of the same const header = headers[headers.length - 1]; @@ -70,35 +90,47 @@ export function init( return null; } }, - getHeaders(): Array<{ name: string, value: string }> { + + getHeaders() { return renderedRequest.headers.map(h => ({ name: h.name, value: h.value, })); }, - hasHeader(name: string): boolean { + + hasHeader(name: string) { return this.getHeader(name) !== null; }, - removeHeader(name: string): void { + + removeHeader(name: string) { const headers = misc.filterHeaders(renderedRequest.headers, name); renderedRequest.headers = renderedRequest.headers.filter(h => !headers.includes(h)); }, - setHeader(name: string, value: string): void { + + setHeader(name: string, value: string) { const header = misc.filterHeaders(renderedRequest.headers, name)[0]; + if (header) { header.value = value; } else { this.addHeader(name, value); } }, - addHeader(name: string, value: string): void { + + addHeader(name: string, value: string) { const header = misc.filterHeaders(renderedRequest.headers, name)[0]; + if (!header) { - renderedRequest.headers.push({ name, value }); + renderedRequest.headers.push({ + name, + value, + }); } }, - getParameter(name: string): string | null { + + getParameter(name: string) { const parameters = misc.filterParameters(renderedRequest.parameters, name); + if (parameters.length) { // Use the last parameter if there are multiple of the same const parameter = parameters[parameters.length - 1]; @@ -107,45 +139,59 @@ export function init( return null; } }, - getParameters(): Array<{ name: string, value: string }> { + + getParameters() { return renderedRequest.parameters.map(p => ({ name: p.name, value: p.value, })); }, - hasParameter(name: string): boolean { + + hasParameter(name: string) { return this.getParameter(name) !== null; }, - removeParameter(name: string): void { + + removeParameter(name: string) { const parameters = misc.filterParameters(renderedRequest.parameters, name); renderedRequest.parameters = renderedRequest.parameters.filter(p => !parameters.includes(p)); }, - setParameter(name: string, value: string): void { + + setParameter(name: string, value: string) { const parameter = misc.filterParameters(renderedRequest.parameters, name)[0]; + if (parameter) { parameter.value = value; } else { this.addParameter(name, value); } }, - addParameter(name: string, value: string): void { + + addParameter(name: string, value: string) { const parameter = misc.filterParameters(renderedRequest.parameters, name)[0]; + if (!parameter) { - renderedRequest.parameters.push({ name, value }); + renderedRequest.parameters.push({ + name, + value, + }); } }, - setAuthenticationParameter(name: string, value: string): void { + + setAuthenticationParameter(name: string, value: string) { Object.assign(renderedRequest.authentication, { [name]: value, }); }, - getAuthentication(): Object { + + getAuthentication() { return renderedRequest.authentication; }, - getBody(): RequestBody { + + getBody() { return renderedRequest.body; }, - setBody(body: RequestBody): void { + + setBody(body: RequestBody) { renderedRequest.body = body; }, @@ -154,43 +200,65 @@ export function init( // ~~~~~~~~~~~~~~~~~~ // /** @deprecated in favor of getting the whole body by getBody */ - getBodyText(): string { + getBodyText() { console.warn('request.getBodyText() is deprecated. Use request.getBody() instead.'); return renderedRequest.body.text || ''; }, /** @deprecated in favor of setting the whole body by setBody */ - setBodyText(text: string): void { + setBodyText(text: string) { console.warn('request.setBodyText() is deprecated. Use request.setBody() instead.'); renderedRequest.body.text = text; }, // NOTE: For these to make sense, we'd need to account for cookies in the jar as well - // addCookie (name: string, value: string): void {} + // addCookie (name: string, value: string) {} // getCookie (name: string): string | null {} - // removeCookie (name: string): void {} - }: Object); + // removeCookie (name: string) {} + }; + /* eslint-disable @typescript-eslint/ban-ts-comment */ if (readOnly) { + // @ts-ignore -- TSCONVERSION something is wrong here, the build doesn't error here but vscode does delete request.setUrl; + // @ts-ignore -- TSCONVERSION something is wrong here, the build doesn't error here but vscode does delete request.setMethod; + // @ts-ignore -- TSCONVERSION something is wrong here, the build doesn't error here but vscode does delete request.setBodyText; + // @ts-ignore -- TSCONVERSION something is wrong here, the build doesn't error here but vscode does delete request.setCookie; + // @ts-ignore -- TSCONVERSION something is wrong here, the build doesn't error here but vscode does delete request.settingSendCookies; + // @ts-ignore -- TSCONVERSION something is wrong here, the build doesn't error here but vscode does delete request.settingStoreCookies; + // @ts-ignore -- TSCONVERSION something is wrong here, the build doesn't error here but vscode does delete request.settingEncodeUrl; + // @ts-ignore -- TSCONVERSION something is wrong here, the build doesn't error here but vscode does delete request.settingDisableRenderRequestBody; + // @ts-ignore -- TSCONVERSION something is wrong here, the build doesn't error here but vscode does delete request.settingFollowRedirects; + // @ts-ignore -- TSCONVERSION something is wrong here, the build doesn't error here but vscode does delete request.removeHeader; + // @ts-ignore -- TSCONVERSION something is wrong here, the build doesn't error here but vscode does delete request.setHeader; + // @ts-ignore -- TSCONVERSION something is wrong here, the build doesn't error here but vscode does delete request.addHeader; + // @ts-ignore -- TSCONVERSION something is wrong here, the build doesn't error here but vscode does delete request.removeParameter; + // @ts-ignore -- TSCONVERSION something is wrong here, the build doesn't error here but vscode does delete request.setParameter; + // @ts-ignore -- TSCONVERSION something is wrong here, the build doesn't error here but vscode does delete request.addParameter; + // @ts-ignore -- TSCONVERSION something is wrong here, the build doesn't error here but vscode does delete request.addParameter; + // @ts-ignore -- TSCONVERSION something is wrong here, the build doesn't error here but vscode does delete request.setAuthenticationParameter; + // @ts-ignore -- TSCONVERSION something is wrong here, the build doesn't error here but vscode does delete request.setBody; } + /* eslint-enable @typescript-eslint/ban-ts-comment */ - return { request }; + return { + request, + }; } diff --git a/packages/insomnia-app/app/plugins/context/response.js b/packages/insomnia-app/app/plugins/context/response.ts similarity index 70% rename from packages/insomnia-app/app/plugins/context/response.js rename to packages/insomnia-app/app/plugins/context/response.ts index bb9a537b94..566aa01009 100644 --- a/packages/insomnia-app/app/plugins/context/response.js +++ b/packages/insomnia-app/app/plugins/context/response.ts @@ -1,21 +1,19 @@ -// @flow import type { ResponseHeader } from '../../models/response'; import * as models from '../../models/index'; import fs from 'fs'; -import { Readable } from 'stream'; -type MaybeResponse = { - parentId?: string, - statusCode?: number, - statusMessage?: string, - bytesRead?: number, - bytesContent?: number, - bodyPath?: string, - elapsedTime?: number, - headers?: Array, -}; +interface MaybeResponse { + parentId?: string; + statusCode?: number; + statusMessage?: string; + bytesRead?: number; + bytesContent?: number; + bodyPath?: string; + elapsedTime?: number; + headers?: Array; +} -export function init(response: MaybeResponse): { response: Object } { +export function init(response?: MaybeResponse) { if (!response) { throw new Error('contexts.response initialized without response'); } @@ -27,27 +25,36 @@ export function init(response: MaybeResponse): { response: Object } { // getId () { // return response.parentId; // }, - getRequestId(): string { + + getRequestId() { return response.parentId || ''; }, - getStatusCode(): number { + + getStatusCode() { return response.statusCode || 0; }, - getStatusMessage(): string { + + getStatusMessage() { return response.statusMessage || ''; }, - getBytesRead(): number { + + getBytesRead() { return response.bytesRead || 0; }, - getTime(): number { + + getTime() { return response.elapsedTime || 0; }, - getBody(): Buffer | null { + + getBody() { return models.response.getBodyBuffer(response); }, - getBodyStream(): Readable | null { + + getBodyStream() { + // @ts-expect-error -- TSCONVERSION return models.response.getBodyStream(response); }, + setBody(body: Buffer) { // Should never happen but just in case it does... if (!response.bodyPath) { @@ -57,9 +64,11 @@ export function init(response: MaybeResponse): { response: Object } { fs.writeFileSync(response.bodyPath, body); response.bytesContent = body.length; }, + getHeader(name: string): string | Array | null { const headers = response.headers || []; const matchedHeaders = headers.filter(h => h.name.toLowerCase() === name.toLowerCase()); + if (matchedHeaders.length > 1) { return matchedHeaders.map(h => h.value); } else if (matchedHeaders.length === 1) { @@ -68,13 +77,15 @@ export function init(response: MaybeResponse): { response: Object } { return null; } }, - getHeaders(): Array<{ name: string, value: string }> { - return response.headers.map(h => ({ + + getHeaders() { + return response.headers?.map(h => ({ name: h.name, value: h.value, })); }, - hasHeader(name: string): boolean { + + hasHeader(name: string) { return this.getHeader(name) !== null; }, }, diff --git a/packages/insomnia-app/app/plugins/context/store.js b/packages/insomnia-app/app/plugins/context/store.js deleted file mode 100644 index 19b106d6df..0000000000 --- a/packages/insomnia-app/app/plugins/context/store.js +++ /dev/null @@ -1,43 +0,0 @@ -// @flow -import type { Plugin } from '../index'; -import * as models from '../../models'; - -export type PluginStore = { - hasItem(string): Promise, - setItem(string, string): Promise, - getItem(string): Promise, - removeItem(string): Promise, - clear(): Promise, - all(): Promise>, -}; - -export function init(plugin: Plugin): { store: PluginStore } { - return { - store: { - async hasItem(key: string): Promise { - const doc = await models.pluginData.getByKey(plugin.name, key); - return doc !== null; - }, - async setItem(key: string, value: string): Promise { - await models.pluginData.upsertByKey(plugin.name, key, String(value)); - }, - async getItem(key: string): Promise { - const doc = await models.pluginData.getByKey(plugin.name, key); - return doc ? doc.value : null; - }, - async removeItem(key: string): Promise { - await models.pluginData.removeByKey(plugin.name, key); - }, - async clear(): Promise { - await models.pluginData.removeAll(plugin.name); - }, - async all(): Promise> { - const docs = await models.pluginData.all(plugin.name); - return docs.map(d => ({ - value: d.value, - key: d.key, - })); - }, - }, - }; -} diff --git a/packages/insomnia-app/app/plugins/context/store.ts b/packages/insomnia-app/app/plugins/context/store.ts new file mode 100644 index 0000000000..13f1e018ec --- /dev/null +++ b/packages/insomnia-app/app/plugins/context/store.ts @@ -0,0 +1,57 @@ +import type { Plugin } from '../index'; +import * as models from '../../models'; + +export interface PluginStore { + hasItem(arg0: string): Promise; + setItem(arg0: string, arg1: string): Promise; + getItem(arg0: string): Promise; + removeItem(arg0: string): Promise; + clear(): Promise; + all(): Promise< + Array<{ + key: string; + value: string; + }> + >; +} + +export function init(plugin: Plugin) { + return { + store: { + async hasItem(key: string) { + const doc = await models.pluginData.getByKey(plugin.name, key); + return doc !== null; + }, + + async setItem(key: string, value: string) { + await models.pluginData.upsertByKey(plugin.name, key, String(value)); + }, + + async getItem(key: string) { + const doc = await models.pluginData.getByKey(plugin.name, key); + return doc ? doc.value : null; + }, + + async removeItem(key: string) { + await models.pluginData.removeByKey(plugin.name, key); + }, + + async clear() { + await models.pluginData.removeAll(plugin.name); + }, + + async all(): Promise< + Array<{ + key: string; + value: string; + }> + > { + const docs = await models.pluginData.all(plugin.name) || []; + return docs.map(d => ({ + value: d.value, + key: d.key, + })); + }, + }, + }; +} diff --git a/packages/insomnia-app/app/plugins/create.js b/packages/insomnia-app/app/plugins/create.ts similarity index 96% rename from packages/insomnia-app/app/plugins/create.js rename to packages/insomnia-app/app/plugins/create.ts index 0f4b57e392..0eed50f21f 100644 --- a/packages/insomnia-app/app/plugins/create.js +++ b/packages/insomnia-app/app/plugins/create.ts @@ -1,4 +1,3 @@ -// @flow import fs from 'fs'; import path from 'path'; import mkdirp from 'mkdirp'; @@ -9,7 +8,7 @@ export async function createPlugin( moduleName: string, version: string, mainJs: string, -): Promise { +) { const pluginDir = path.join(PLUGIN_PATH, moduleName); if (fs.existsSync(pluginDir)) { @@ -18,7 +17,6 @@ export async function createPlugin( rimraf.sync(pluginDir); mkdirp.sync(pluginDir); - // Write package.json fs.writeFileSync( path.join(pluginDir, 'package.json'), @@ -37,7 +35,6 @@ export async function createPlugin( 2, ), ); - // Write main JS file fs.writeFileSync(path.join(pluginDir, 'main.js'), mainJs); } diff --git a/packages/insomnia-app/app/plugins/index.js b/packages/insomnia-app/app/plugins/index.ts similarity index 60% rename from packages/insomnia-app/app/plugins/index.js rename to packages/insomnia-app/app/plugins/index.ts index fbe1923df6..0f751a111c 100644 --- a/packages/insomnia-app/app/plugins/index.js +++ b/packages/insomnia-app/app/plugins/index.ts @@ -1,6 +1,5 @@ -// @flow import mkdirp from 'mkdirp'; -import { appConfig } from '../../config'; +import appConfig from '../../config/config.json'; import * as models from '../models'; import fs from 'fs'; import path from 'path'; @@ -13,103 +12,119 @@ import type { RequestGroup } from '../models/request-group'; import type { Request } from '../models/request'; import type { PluginConfig, PluginConfigMap } from '../models/settings'; import type { Workspace } from '../models/workspace'; +import { GrpcRequest } from '../models/grpc-request'; -export type Plugin = { - name: string, - description: string, - version: string, - directory: string, - config: PluginConfig, - module: *, -}; +export interface Module { + templateTags?: Array, + requestHooks?: Array<(requstContext: any) => void>, + responseHooks?: Array<(responseContext: any) => void>, + themes?: Array, + requestGroupActions?: Array>, + requestActions?: Array>, + workspaceActions?: Array>, + documentActions?: Array>, + configGenerators?: Array>, +} -export type TemplateTag = { - plugin: Plugin, - templateTag: PluginTemplateTag, -}; +export interface Plugin { + name: string; + description: string; + version: string; + directory: string; + config: PluginConfig; + module: Module; +} +interface InternalProperties { + plugin: Plugin; +} -export type RequestGroupAction = { - plugin: Plugin, +type OmitInternal = Omit; +export interface TemplateTag extends InternalProperties { + templateTag: PluginTemplateTag; +} + +export interface RequestGroupAction extends InternalProperties { action: ( - context: Object, + context: Record, models: { - requestGroup: RequestGroup, - requests: Array, + requestGroup: RequestGroup; + requests: Array; }, - ) => void | Promise, - label: string, - icon?: string, -}; + ) => void | Promise; + label: string; + icon?: string; +} -export type RequestAction = { - plugin: Plugin, +export interface RequestAction extends InternalProperties { action: ( - context: Object, + context: Record, models: { - requestGroup: RequestGroup, - request: Array, + requestGroup?: RequestGroup; + request: Request | GrpcRequest; }, - ) => void | Promise, - label: string, - icon?: string, -}; + ) => void | Promise; + label: string; + icon?: string; +} -export type WorkspaceAction = { - plugin: Plugin, +export interface WorkspaceAction extends InternalProperties { action: ( - context: Object, + context: Record, models: { - workspace: Workspace, - requestGroups: Array, - requests: Array, + workspace: Workspace; + requestGroups: Array; + requests: Array; }, - ) => void | Promise, - label: string, - icon?: string, -}; + ) => void | Promise; + label: string; + icon?: string; +} -export type SpecInfo = { - contents: Object, - rawContents: string, - format: string, - formatVersion: string, -}; +export interface SpecInfo { + contents: Record; + rawContents: string; + format: string; + formatVersion: string; +} -export type ConfigGenerator = { - plugin: Plugin, - label: string, - generate: (info: SpecInfo) => Promise<{ document?: string, error?: string }>, -}; +export interface ConfigGenerator extends InternalProperties { + label: string; + generate: ( + info: SpecInfo, + ) => Promise<{ + document?: string; + error?: string; + }>; +} -export type DocumentAction = { - plugin: Plugin, - action: (context: Object, documents: SpecInfo) => void | Promise, - label: string, - hideAfterClick?: boolean, -}; +export interface DocumentAction extends InternalProperties { + action: (context: Record, documents: SpecInfo) => void | Promise; + label: string; + hideAfterClick?: boolean; +} -export type RequestHook = { - plugin: Plugin, - hook: Function, -}; +type RequestHookCallback = (context: any) => void; -export type ResponseHook = { - plugin: Plugin, - hook: Function, -}; +export interface RequestHook extends InternalProperties { + hook: RequestHookCallback; +} -export type Theme = { - plugin: Plugin, - theme: PluginTheme, -}; +type ResponseHookCallback = (context: any) => void; +export interface ResponseHook extends InternalProperties { + hook: ResponseHookCallback; +} + +export interface Theme extends InternalProperties { + theme: PluginTheme; +} export type ColorScheme = 'default' | 'light' | 'dark'; -let plugins: ?Array = null; +let plugins: Array | null | undefined = null; let ignorePlugins: Array = []; -export async function init(): Promise { +export async function init() { clearIgnores(); await reloadPlugins(); } @@ -125,7 +140,7 @@ export function clearIgnores() { } async function _traversePluginPath( - pluginMap: Object, + pluginMap: Record, allPaths: Array, allConfigs: PluginConfigMap, ) { @@ -173,7 +188,6 @@ async function _traversePluginPath( const module = global.require(modulePath); const pluginName = pluginJson.name; - pluginMap[pluginName] = _initPlugin(pluginJson || {}, module, allConfigs, modulePath); console.log(`[plugin] Loaded ${modulePath}`); } catch (err) { @@ -187,7 +201,7 @@ async function _traversePluginPath( } } -export async function getPlugins(force: boolean = false): Promise> { +export async function getPlugins(force = false): Promise> { if (force) { plugins = null; } @@ -199,43 +213,43 @@ export async function getPlugins(force: boolean = false): Promise> .split(':') .filter(p => p) .map(resolveHomePath); - // Make sure the default directories exist mkdirp.sync(PLUGIN_PATH); - // Also look in node_modules folder in each directory const basePaths = [PLUGIN_PATH, ...extraPaths]; const extendedPaths = basePaths.map(p => path.join(p, 'node_modules')); const allPaths = [...basePaths, ...extendedPaths]; - // Store plugins in a map so that plugins with the same // name only get added once // TODO: Make this more complex and have the latest version always win - const pluginMap: { [string]: Plugin } = { + const pluginMap: Record = { // "name": "module" }; - for (const p of appConfig().plugins) { + for (const p of appConfig.plugins) { if (ignorePlugins.includes(p)) { continue; } + const pluginJson = global.require(`${p}/package.json`); + if (ignorePlugins.includes(pluginJson.name)) { continue; } + const pluginModule = global.require(p); + pluginMap[pluginJson.name] = _initPlugin(pluginJson, pluginModule, allConfigs); } await _traversePluginPath(pluginMap, allPaths, allConfigs); - plugins = Object.keys(pluginMap).map(name => pluginMap[name]); } return plugins; } -export async function reloadPlugins(): Promise { +export async function reloadPlugins() { await getPlugins(true); } @@ -244,113 +258,173 @@ async function getActivePlugins(): Promise> { } export async function getRequestGroupActions(): Promise> { - let extensions = []; + let extensions: Array = []; + for (const plugin of await getActivePlugins()) { const actions = plugin.module.requestGroupActions || []; - extensions = [...extensions, ...actions.map(p => ({ plugin, ...p }))]; + extensions = [ + ...extensions, + ...actions.map(p => ({ + plugin, + ...p, + })), + ]; } return extensions; } export async function getRequestActions(): Promise> { - let extensions = []; + let extensions: Array = []; + for (const plugin of await getActivePlugins()) { const actions = plugin.module.requestActions || []; - extensions = [...extensions, ...actions.map(p => ({ plugin, ...p }))]; + extensions = [ + ...extensions, + ...actions.map(p => ({ + plugin, + ...p, + })), + ]; } return extensions; } export async function getWorkspaceActions(): Promise> { - let extensions = []; + let extensions: Array = []; + for (const plugin of await getActivePlugins()) { const actions = plugin.module.workspaceActions || []; - extensions = [...extensions, ...actions.map(p => ({ plugin, ...p }))]; + extensions = [ + ...extensions, + ...actions.map(p => ({ + plugin, + ...p, + })), + ]; } return extensions; } export async function getDocumentActions(): Promise> { - let extensions = []; + let extensions: Array = []; + for (const plugin of await getActivePlugins()) { const actions = plugin.module.documentActions || []; - extensions = [...extensions, ...actions.map(p => ({ plugin, ...p }))]; + extensions = [ + ...extensions, + ...actions.map(p => ({ + plugin, + ...p, + })), + ]; } return extensions; } export async function getTemplateTags(): Promise> { - let extensions = []; + let extensions: Array = []; + for (const plugin of await getActivePlugins()) { const templateTags = plugin.module.templateTags || []; - extensions = [...extensions, ...templateTags.map(tt => ({ plugin, templateTag: tt }))]; + extensions = [ + ...extensions, + ...templateTags.map(tt => ({ + plugin, + templateTag: tt, + })), + ]; } return extensions; } export async function getRequestHooks(): Promise> { - let functions = []; + let functions: Array = []; + for (const plugin of await getActivePlugins()) { const moreFunctions = plugin.module.requestHooks || []; - functions = [...functions, ...moreFunctions.map(hook => ({ plugin, hook }))]; + functions = [ + ...functions, + ...moreFunctions.map(hook => ({ + plugin, + hook, + })), + ]; } return functions; } export async function getResponseHooks(): Promise> { - let functions = []; + let functions: Array = []; + for (const plugin of await getActivePlugins()) { const moreFunctions = plugin.module.responseHooks || []; - functions = [...functions, ...moreFunctions.map(hook => ({ plugin, hook }))]; + functions = [ + ...functions, + ...moreFunctions.map(hook => ({ + plugin, + hook, + })), + ]; } return functions; } export async function getThemes(): Promise> { - let extensions = []; + let extensions: Array = []; + for (const plugin of await getActivePlugins()) { const themes = plugin.module.themes || []; - extensions = [...extensions, ...themes.map(theme => ({ plugin, theme }))]; + extensions = [ + ...extensions, + ...themes.map(theme => ({ + plugin, + theme, + })), + ]; } return extensions; } export async function getConfigGenerators(): Promise> { - let functions = []; + let functions: Array = []; + for (const plugin of await getActivePlugins()) { const moreFunctions = plugin.module.configGenerators || []; - functions = [...functions, ...moreFunctions.map(p => ({ plugin, ...p }))]; + functions = [ + ...functions, + ...moreFunctions.map(p => ({ + plugin, + ...p, + })), + ]; } return functions; } - const _defaultPluginConfig: PluginConfig = { disabled: false, }; function _initPlugin( - packageJSON: Object, + packageJSON: Record, module: any, allConfigs: PluginConfigMap, - path: ?string, + path?: string | null, ): Plugin { const meta = packageJSON.insomnia || {}; const name = packageJSON.name || meta.name; - // Find config const config: PluginConfig = allConfigs.hasOwnProperty(name) ? allConfigs[name] : _defaultPluginConfig; - return { name, description: packageJSON.description || meta.description || '', diff --git a/packages/insomnia-app/app/plugins/install.js b/packages/insomnia-app/app/plugins/install.ts similarity index 84% rename from packages/insomnia-app/app/plugins/install.js rename to packages/insomnia-app/app/plugins/install.ts index ebdb0d14ee..6234cb680e 100644 --- a/packages/insomnia-app/app/plugins/install.js +++ b/packages/insomnia-app/app/plugins/install.ts @@ -1,23 +1,32 @@ -// @flow import * as electron from 'electron'; import fs from 'fs'; import fsx from 'fs-extra'; import childProcess from 'child_process'; -import { getTempDir, isDevelopment, isWindows, PLUGIN_PATH } from '../common/constants'; +import { isDevelopment, isWindows, PLUGIN_PATH } from '../common/constants'; import mkdirp from 'mkdirp'; import path from 'path'; +import { getTempDir } from '../common/electron-helpers'; const YARN_DEPRECATED_WARN = /(?warning)(?[^>:].+[>:])(?.+)/; -export default async function(lookupName: string): Promise { - return new Promise(async (resolve, reject) => { - let info: Object = {}; +interface InsomniaPlugin { + insomnia: any; + name: string; + version: string; + dist: { + shasum: string; + tarball: string; + } +} + +export default async function(lookupName: string) { + return new Promise(async (resolve, reject) => { + let info: InsomniaPlugin | null = null; + try { info = await _isInsomniaPlugin(lookupName); - // Get actual module name without version suffixes and things const moduleName = info.name; - const pluginDir = path.join(PLUGIN_PATH, moduleName); // Make plugin directory @@ -26,28 +35,33 @@ export default async function(lookupName: string): Promise { // Download the module const request = electron.remote.net.request(info.dist.tarball); request.on('error', err => { - reject(new Error(`Failed to make plugin request ${info.dist.tarball}: ${err.message}`)); + reject(new Error(`Failed to make plugin request ${info?.dist.tarball}: ${err.message}`)); }); - const { tmpDir } = await _installPluginToTmpDir(lookupName); console.log(`[plugins] Moving plugin from ${tmpDir} to ${pluginDir}`); // Move entire module to plugins folder - fsx.moveSync(path.join(tmpDir, moduleName), pluginDir, { - overwrite: true, - }); + fsx.moveSync( + path.join(tmpDir, moduleName), + pluginDir, + { overwrite: true }, + ); // Move each dependency into node_modules folder const pluginModulesDir = path.join(pluginDir, 'node_modules'); mkdirp.sync(pluginModulesDir); + for (const name of fs.readdirSync(tmpDir)) { const src = path.join(tmpDir, name); + if (name === moduleName || !fs.statSync(src).isDirectory()) { continue; } const dest = path.join(pluginModulesDir, name); - fsx.moveSync(src, dest, { overwrite: true }); + fsx.moveSync(src, dest, { + overwrite: true, + }); } } catch (err) { reject(err); @@ -58,8 +72,8 @@ export default async function(lookupName: string): Promise { }); } -async function _isInsomniaPlugin(lookupName: string): Promise { - return new Promise((resolve, reject) => { +async function _isInsomniaPlugin(lookupName: string) { + return new Promise((resolve, reject) => { console.log('[plugins] Fetching module info from npm'); childProcess.execFile( escape(process.execPath), @@ -86,6 +100,7 @@ async function _isInsomniaPlugin(lookupName: string): Promise { } let yarnOutput; + try { yarnOutput = JSON.parse(stdout.toString()); } catch (ex) { @@ -97,18 +112,19 @@ async function _isInsomniaPlugin(lookupName: string): Promise { } else { reject(new Error(`Yarn response not JSON: ${ex.message}`)); } + return; } const data = yarnOutput.data; + if (!data.hasOwnProperty('insomnia')) { reject(new Error(`"${lookupName}" not a plugin! Package missing "insomnia" attribute`)); return; } console.log(`[plugins] Detected Insomnia plugin ${data.name}`); - - resolve({ + const insomniaPlugin: InsomniaPlugin = { insomnia: data.insomnia, name: data.name, version: data.version, @@ -116,14 +132,15 @@ async function _isInsomniaPlugin(lookupName: string): Promise { shasum: data.dist.shasum, tarball: data.dist.tarball, }, - }); + }; + resolve(insomniaPlugin); }, ); }); } -async function _installPluginToTmpDir(lookupName: string): Promise<{ tmpDir: string }> { - return new Promise((resolve, reject) => { +async function _installPluginToTmpDir(lookupName: string) { + return new Promise<{ tmpDir: string }>((resolve, reject) => { const tmpDir = path.join(getTempDir(), `${lookupName}-${Date.now()}`); mkdirp.sync(tmpDir); console.log(`[plugins] Installing plugin to ${tmpDir}`); @@ -146,7 +163,8 @@ async function _installPluginToTmpDir(lookupName: string): Promise<{ tmpDir: str timeout: 5 * 60 * 1000, maxBuffer: 1024 * 1024, cwd: tmpDir, - shell: true, // Some package installs require a shell + shell: true, + // Some package installs require a shell env: { NODE_ENV: 'production', ELECTRON_RUN_AS_NODE: 'true', @@ -166,7 +184,9 @@ async function _installPluginToTmpDir(lookupName: string): Promise<{ tmpDir: str return; } - resolve({ tmpDir }); + resolve({ + tmpDir, + }); }, ); }); @@ -190,9 +210,9 @@ export function containsOnlyDeprecationWarnings(stderr) { * @param str The error message * @returns {boolean} Returns true if it's a deprecated warning */ -export function isDeprecatedDependencies(str: string): boolean { +export function isDeprecatedDependencies(str: string) { // The issue contains the message as it is without the dependency list - const message = YARN_DEPRECATED_WARN.exec(str)?.groups.issue; + const message = YARN_DEPRECATED_WARN.exec(str)?.groups?.issue; // Strict check, everything must be matched to be a false positive // !! is not a mistake, makes it returns boolean instead of undefined on error return !!( diff --git a/packages/insomnia-app/app/plugins/misc.js b/packages/insomnia-app/app/plugins/misc.ts similarity index 82% rename from packages/insomnia-app/app/plugins/misc.js rename to packages/insomnia-app/app/plugins/misc.ts index 18c0274811..6d76828a29 100644 --- a/packages/insomnia-app/app/plugins/misc.js +++ b/packages/insomnia-app/app/plugins/misc.ts @@ -1,68 +1,69 @@ -// @flow import Color from 'color'; import { render, THROW_ON_ERROR } from '../common/render'; import { getThemes } from './index'; import type { Theme } from './index'; import { getAppDefaultTheme } from '../common/constants'; +import { Settings } from '../models/settings'; -type ThemeBlock = { +interface ThemeBlock { background?: { - default: string, - success?: string, - notice?: string, - warning?: string, - danger?: string, - surprise?: string, - info?: string, - }, + default: string; + success?: string; + notice?: string; + warning?: string; + danger?: string; + surprise?: string; + info?: string; + }; foreground?: { - default: string, - success?: string, - notice?: string, - warning?: string, - danger?: string, - surprise?: string, - info?: string, - }, + default: string; + success?: string; + notice?: string; + warning?: string; + danger?: string; + surprise?: string; + info?: string; + }; highlight?: { - default: string, - xxs?: string, - xs?: string, - sm?: string, - md?: string, - lg?: string, - xl?: string, - }, + default: string; + xxs?: string; + xs?: string; + sm?: string; + md?: string; + lg?: string; + xl?: string; + }; +} + +type ThemeInner = ThemeBlock & { + rawCss?: string; + styles?: + | { + dialog?: ThemeBlock; + dialogFooter?: ThemeBlock; + dialogHeader?: ThemeBlock; + dropdown?: ThemeBlock; + editor?: ThemeBlock; + link?: ThemeBlock; + overlay?: ThemeBlock; + pane?: ThemeBlock; + paneHeader?: ThemeBlock; + sidebar?: ThemeBlock; + sidebarHeader?: ThemeBlock; + sidebarList?: ThemeBlock; + tooltip?: ThemeBlock; + transparentOverlay?: ThemeBlock; + } + | null; }; -type ThemeInner = { - ...ThemeBlock, - rawCss?: string, - styles: ?{ - dialog?: ThemeBlock, - dialogFooter?: ThemeBlock, - dialogHeader?: ThemeBlock, - dropdown?: ThemeBlock, - editor?: ThemeBlock, - link?: ThemeBlock, - overlay?: ThemeBlock, - pane?: ThemeBlock, - paneHeader?: ThemeBlock, - sidebar?: ThemeBlock, - sidebarHeader?: ThemeBlock, - sidebarList?: ThemeBlock, - tooltip?: ThemeBlock, - transparentOverlay?: ThemeBlock, - }, -}; +export interface PluginTheme { + name: string; + displayName: string; + theme: ThemeInner; +} -export type PluginTheme = { - name: string, - displayName: string, - theme: ThemeInner, -}; - -export async function generateThemeCSS(theme: PluginTheme): Promise { +export async function generateThemeCSS(theme: PluginTheme) { const renderedTheme: ThemeInner = await render( theme.theme, theme.theme, @@ -71,9 +72,7 @@ export async function generateThemeCSS(theme: PluginTheme): Promise { theme.name, ); const n = theme.name; - let css = ''; - // For the top-level variables, merge with the base theme to ensure that // we have everything we need. css += wrapStyles( @@ -89,46 +88,37 @@ export async function generateThemeCSS(theme: PluginTheme): Promise { if (renderedTheme.styles) { const styles = renderedTheme.styles; - // Dropdown Menus css += wrapStyles( n, '.theme--dropdown__menu', getThemeBlockCSS(styles.dropdown || styles.dialog), ); - // Tooltips css += wrapStyles(n, '.theme--tooltip', getThemeBlockCSS(styles.tooltip || styles.dialog)); - // Overlay css += wrapStyles( n, '.theme--transparent-overlay', getThemeBlockCSS(styles.transparentOverlay), ); - // Dialogs css += wrapStyles(n, '.theme--dialog', getThemeBlockCSS(styles.dialog)); css += wrapStyles(n, '.theme--dialog__header', getThemeBlockCSS(styles.dialogHeader)); css += wrapStyles(n, '.theme--dialog__footer', getThemeBlockCSS(styles.dialogFooter)); - // Panes css += wrapStyles(n, '.theme--pane', getThemeBlockCSS(styles.pane)); css += wrapStyles(n, '.theme--pane__header', getThemeBlockCSS(styles.paneHeader)); - + // @ts-expect-error -- TSCONVERSION css += wrapStyles(n, '.theme--app-header', getThemeBlockCSS(styles.appHeader)); - // Sidebar Styles css += wrapStyles(n, '.theme--sidebar', getThemeBlockCSS(styles.sidebar)); css += wrapStyles(n, '.theme--sidebar__list', getThemeBlockCSS(styles.sidebarList)); css += wrapStyles(n, '.theme--sidebar__header', getThemeBlockCSS(styles.sidebarHeader)); - // Link css += wrapStyles(n, '.theme--link', getThemeBlockCSS(styles.link)); - // Code Editors css += wrapStyles(n, '.theme--editor', getThemeBlockCSS(styles.editor)); - // HACK: Dialog styles for CodeMirror dialogs too css += wrapStyles(n, '.CodeMirror-info', getThemeBlockCSS(styles.dialog)); } @@ -136,13 +126,12 @@ export async function generateThemeCSS(theme: PluginTheme): Promise { return css; } -function getThemeBlockCSS(block?: ThemeBlock): string { +function getThemeBlockCSS(block?: ThemeBlock) { if (!block) { return ''; } const indent = '\t'; - let css = ''; const addColorVar = (variable: string, value?: string) => { @@ -164,6 +153,7 @@ function getThemeBlockCSS(block?: ThemeBlock): string { if (!value) { return; } + css += `${indent}--${variable}: ${value};\n`; }; @@ -232,24 +222,25 @@ function wrapStyles(theme: string, selector: string, styles: string) { ].join('\n'); } -export function getColorScheme(settings: Settings): ColorScheme { +export function getColorScheme(settings: Settings) { if (!settings.autoDetectColorScheme) { return 'default'; } - if (window.matchMedia(`(prefers-color-scheme: light)`).matches) { + if (window.matchMedia('(prefers-color-scheme: light)').matches) { return 'light'; } - if (window.matchMedia(`(prefers-color-scheme: dark)`).matches) { + if (window.matchMedia('(prefers-color-scheme: dark)').matches) { return 'dark'; } return 'default'; } -export async function applyColorScheme(settings: Settings): Promise { +export async function applyColorScheme(settings: Settings) { const scheme = getColorScheme(settings); + if (scheme === 'light') { await setTheme(settings.lightTheme); } else if (scheme === 'dark') { @@ -285,7 +276,6 @@ export async function setTheme(themeName: string) { let themeCSS = (await generateThemeCSS(theme.theme)) + '\n'; const { name } = theme.theme; const { rawCss } = theme.theme.theme; - let s = document.querySelector(`style[data-theme-name="${name}"]`); if (!s) { @@ -302,7 +292,7 @@ export async function setTheme(themeName: string) { } } -export async function setFont(settings: Object) { +export async function setFont(settings: Record) { if (!document) { return; } diff --git a/packages/insomnia-app/app/renderer.html b/packages/insomnia-app/app/renderer.html index 53de80bc9e..6ac4ca1cf1 100644 --- a/packages/insomnia-app/app/renderer.html +++ b/packages/insomnia-app/app/renderer.html @@ -11,7 +11,14 @@
diff --git a/packages/insomnia-app/app/renderer.js b/packages/insomnia-app/app/renderer.ts similarity index 100% rename from packages/insomnia-app/app/renderer.js rename to packages/insomnia-app/app/renderer.ts diff --git a/packages/insomnia-app/app/sync/delta/__tests__/diff.test.js b/packages/insomnia-app/app/sync/delta/__tests__/diff.test.ts similarity index 70% rename from packages/insomnia-app/app/sync/delta/__tests__/diff.test.js rename to packages/insomnia-app/app/sync/delta/__tests__/diff.test.ts index 8e2398c9cc..60221162b1 100644 --- a/packages/insomnia-app/app/sync/delta/__tests__/diff.test.js +++ b/packages/insomnia-app/app/sync/delta/__tests__/diff.test.ts @@ -3,6 +3,7 @@ import { diff, __internal } from '../diff'; describe('diff()', () => { it('creates block map', () => { const result = __internal.getBlockMap('Hello World!!', 3); + expect(result).toEqual({ dbc2d1fed0dc37a70aea0f376958c802eddc0559: [ { @@ -46,9 +47,20 @@ describe('diff()', () => { // hel|lo |Wor|ld!|! const result = diff('Hello World!!', 'Hello there World!!', 3); expect(result).toEqual([ - { type: 'COPY', start: 0, len: 6 }, - { type: 'INSERT', content: 'there ' }, - { type: 'COPY', start: 6, len: 7 }, + { + type: 'COPY', + start: 0, + len: 6, + }, + { + type: 'INSERT', + content: 'there ', + }, + { + type: 'COPY', + start: 6, + len: 7, + }, ]); }); @@ -59,12 +71,33 @@ describe('diff()', () => { 2, ); expect(result2).toEqual([ - { len: 24, start: 0, type: 'COPY' }, - { content: 'shor', type: 'INSERT' }, - { len: 2, start: 42, type: 'COPY' }, - { content: 's', type: 'INSERT' }, - { len: 7, start: 30, type: 'COPY' }, - { content: '.', type: 'INSERT' }, + { + len: 24, + start: 0, + type: 'COPY', + }, + { + content: 'shor', + type: 'INSERT', + }, + { + len: 2, + start: 42, + type: 'COPY', + }, + { + content: 's', + type: 'INSERT', + }, + { + len: 7, + start: 30, + type: 'COPY', + }, + { + content: '.', + type: 'INSERT', + }, ]); }); }); diff --git a/packages/insomnia-app/app/sync/delta/__tests__/patch.test.js b/packages/insomnia-app/app/sync/delta/__tests__/patch.test.ts similarity index 100% rename from packages/insomnia-app/app/sync/delta/__tests__/patch.test.js rename to packages/insomnia-app/app/sync/delta/__tests__/patch.test.ts index 0a2624ed06..122119381b 100644 --- a/packages/insomnia-app/app/sync/delta/__tests__/patch.test.js +++ b/packages/insomnia-app/app/sync/delta/__tests__/patch.test.ts @@ -18,6 +18,7 @@ describe('patch()', () => { // Test both directions for (let j = 0; j < 2; j++) { let a, b; + if (j === 0) { a = thing[0]; b = thing[1]; @@ -28,7 +29,6 @@ describe('patch()', () => { // Diff the result const diffResult = diff(a, b, i * 5); - // Use the diff to patch it and make sure it equals the same expect(patch(a, diffResult)).toBe(b); } diff --git a/packages/insomnia-app/app/sync/delta/diff.js b/packages/insomnia-app/app/sync/delta/diff.ts similarity index 85% rename from packages/insomnia-app/app/sync/delta/diff.js rename to packages/insomnia-app/app/sync/delta/diff.ts index 60533ff793..0a5f0ff20c 100644 --- a/packages/insomnia-app/app/sync/delta/diff.js +++ b/packages/insomnia-app/app/sync/delta/diff.ts @@ -1,36 +1,35 @@ -// @flow import crypto from 'crypto'; -type InsertOperation = {| - type: 'INSERT', - content: string, -|}; +interface InsertOperation { + type: 'INSERT'; + content: string; +} -type CopyOperation = {| - type: 'COPY', - start: number, - len: number, -|}; +interface CopyOperation { + type: 'COPY'; + start: number; + len: number; +} export type Operation = InsertOperation | CopyOperation; -type Block = {| - start: number, - len: number, - hash: string, -|}; +interface Block { + start: number; + len: number; + hash: string; +} export function diff(source: string, target: string, blockSize: number): Array { const operations: Array = []; - const sourceBlockMap = getBlockMap(source, blockSize); - // Iterate over source blocks in order and match them to target let lastTargetMatch = 0; + for (let targetPosition = 0; targetPosition < target.length; ) { const targetBlock = getBlock(target, targetPosition, blockSize); const sourceBlocks = sourceBlockMap[targetBlock.hash] || []; + // @ts-expect-error -- TSCONVERSION appears to be a genuine error if (sourceBlocks.length === 0) { targetPosition++; continue; @@ -38,7 +37,6 @@ export function diff(source: string, target: string, blockSize: number): Array { const map = {}; for (let i = 0; i < value.length; ) { diff --git a/packages/insomnia-app/app/sync/delta/patch.js b/packages/insomnia-app/app/sync/delta/patch.ts similarity index 77% rename from packages/insomnia-app/app/sync/delta/patch.js rename to packages/insomnia-app/app/sync/delta/patch.ts index c5356182c8..7337ec3685 100644 --- a/packages/insomnia-app/app/sync/delta/patch.js +++ b/packages/insomnia-app/app/sync/delta/patch.ts @@ -1,7 +1,6 @@ -// @flow import type { Operation } from './diff'; -export function patch(a: string, operations: Array): string { +export function patch(a: string, operations: Array) { let result = ''; for (const op of operations) { diff --git a/packages/insomnia-app/app/sync/git/__mocks__/path.js b/packages/insomnia-app/app/sync/git/__mocks__/path.ts similarity index 57% rename from packages/insomnia-app/app/sync/git/__mocks__/path.js rename to packages/insomnia-app/app/sync/git/__mocks__/path.ts index ab0248c410..fa737c9969 100644 --- a/packages/insomnia-app/app/sync/git/__mocks__/path.js +++ b/packages/insomnia-app/app/sync/git/__mocks__/path.ts @@ -6,7 +6,10 @@ const exportObj = { __mockPath, ...path }; function __mockPath(type) { const mock = type === 'win32' ? path.win32 : path.posix; - Object.keys(mock).forEach(k => (exportObj[k] = mock[k])); + Object.keys(mock).forEach(k => { + exportObj[k] = mock[k]; + }); } +// WARNING: changing this to `export default` will break the mock and be incredibly hard to debug. Ask me how I know. module.exports = exportObj; diff --git a/packages/insomnia-app/app/sync/git/__mocks__/shallow-clone.js b/packages/insomnia-app/app/sync/git/__mocks__/shallow-clone.js deleted file mode 100644 index 8cd8240056..0000000000 --- a/packages/insomnia-app/app/sync/git/__mocks__/shallow-clone.js +++ /dev/null @@ -1,3 +0,0 @@ -module.exports = { - shallowClone: jest.fn(), -}; diff --git a/packages/insomnia-app/app/sync/git/__mocks__/shallow-clone.ts b/packages/insomnia-app/app/sync/git/__mocks__/shallow-clone.ts new file mode 100644 index 0000000000..3feba033c9 --- /dev/null +++ b/packages/insomnia-app/app/sync/git/__mocks__/shallow-clone.ts @@ -0,0 +1,4 @@ +// WARNING: changing this to `export default` will break the mock and be incredibly hard to debug. Ask me how I know. +module.exports = { + shallowClone: jest.fn(), +}; diff --git a/packages/insomnia-app/app/sync/git/__tests__/git-rollback.test.js b/packages/insomnia-app/app/sync/git/__tests__/git-rollback.test.ts similarity index 79% rename from packages/insomnia-app/app/sync/git/__tests__/git-rollback.test.js rename to packages/insomnia-app/app/sync/git/__tests__/git-rollback.test.ts index 4291c1cea6..0996e5c91b 100644 --- a/packages/insomnia-app/app/sync/git/__tests__/git-rollback.test.js +++ b/packages/insomnia-app/app/sync/git/__tests__/git-rollback.test.ts @@ -1,4 +1,3 @@ -// @flow import path from 'path'; import { GitVCS, GIT_CLONE_DIR, GIT_INSOMNIA_DIR } from '../git-vcs'; import { setupDateMocks } from './util'; @@ -8,14 +7,19 @@ import { gitRollback } from '../git-rollback'; describe('git rollback', () => { describe('mocked', () => { - const removeMock = jest.fn().mockResolvedValue(); - const unlinkMock = jest.fn().mockResolvedValue(); - const undoPendingChangesMock = jest.fn().mockResolvedValue(); - let vcs: $Shape = {}; + const removeMock = jest.fn().mockResolvedValue(undefined); + const unlinkMock = jest.fn().mockResolvedValue(undefined); + const undoPendingChangesMock = jest.fn().mockResolvedValue(undefined); + + let vcs: Partial = {}; beforeEach(() => { jest.resetAllMocks(); - const fsMock = { promises: { unlink: unlinkMock } }; + const fsMock = { + promises: { + unlink: unlinkMock, + }, + }; vcs = { getFs: jest.fn().mockReturnValue(fsMock), remove: removeMock, @@ -26,39 +30,42 @@ describe('git rollback', () => { it('should remove and delete added and *added files', async () => { const aTxt = 'a.txt'; const bTxt = 'b.txt'; - const files: Array = [ - { filePath: aTxt, status: 'added' }, - { filePath: bTxt, status: '*added' }, + { + filePath: aTxt, + status: 'added', + }, + { + filePath: bTxt, + status: '*added', + }, ]; - await gitRollback(vcs, files); - expect(unlinkMock).toHaveBeenCalledTimes(2); expect(unlinkMock).toHaveBeenNthCalledWith(1, aTxt); expect(unlinkMock).toHaveBeenNthCalledWith(2, bTxt); - expect(removeMock).toHaveBeenCalledTimes(2); expect(removeMock).toHaveBeenNthCalledWith(1, aTxt); expect(removeMock).toHaveBeenNthCalledWith(2, bTxt); - expect(undoPendingChangesMock).not.toHaveBeenCalled(); }); it('should undo pending changes for non-added files', async () => { const aTxt = 'a.txt'; const bTxt = 'b.txt'; - const files: Array = [ - { filePath: aTxt, status: 'modified' }, - { filePath: bTxt, status: 'deleted' }, + { + filePath: aTxt, + status: 'modified', + }, + { + filePath: bTxt, + status: 'deleted', + }, ]; - await gitRollback(vcs, files); - expect(unlinkMock).toHaveBeenCalledTimes(0); expect(removeMock).toHaveBeenCalledTimes(0); - expect(undoPendingChangesMock).toHaveBeenCalledTimes(1); expect(undoPendingChangesMock).toHaveBeenCalledWith(expect.arrayContaining([aTxt, bTxt])); }); @@ -68,24 +75,31 @@ describe('git rollback', () => { const bTxt = 'b.txt'; const cTxt = 'c.txt'; const dTxt = 'd.txt'; - const files: Array = [ - { filePath: aTxt, status: 'added' }, - { filePath: bTxt, status: '*added' }, - { filePath: cTxt, status: 'modified' }, - { filePath: dTxt, status: 'deleted' }, + { + filePath: aTxt, + status: 'added', + }, + { + filePath: bTxt, + status: '*added', + }, + { + filePath: cTxt, + status: 'modified', + }, + { + filePath: dTxt, + status: 'deleted', + }, ]; - await gitRollback(vcs, files); - expect(unlinkMock).toHaveBeenCalledTimes(2); expect(unlinkMock).toHaveBeenNthCalledWith(1, aTxt); expect(unlinkMock).toHaveBeenNthCalledWith(2, bTxt); - expect(removeMock).toHaveBeenCalledTimes(2); expect(removeMock).toHaveBeenNthCalledWith(1, aTxt); expect(removeMock).toHaveBeenNthCalledWith(2, bTxt); - expect(undoPendingChangesMock).toHaveBeenCalledTimes(1); expect(undoPendingChangesMock).toHaveBeenCalledWith(expect.arrayContaining([cTxt, dTxt])); }); @@ -100,54 +114,55 @@ describe('git rollback', () => { barTxt = path.join(GIT_INSOMNIA_DIR, 'bar.txt'); bazTxt = path.join(GIT_INSOMNIA_DIR, 'baz.txt'); }); - afterAll(() => jest.restoreAllMocks()); beforeEach(setupDateMocks); it('should rollback files as expected', async () => { const originalContent = 'original'; - const fsClient = MemClient.createClient(); await fsClient.promises.mkdir(GIT_INSOMNIA_DIR); await fsClient.promises.writeFile(fooTxt, 'foo'); await fsClient.promises.writeFile(barTxt, 'bar'); await fsClient.promises.writeFile(bazTxt, originalContent); - const vcs = new GitVCS(); - await vcs.init({ directory: GIT_CLONE_DIR, fs: fsClient }); - + await vcs.init({ + directory: GIT_CLONE_DIR, + fs: fsClient, + }); // Commit await vcs.setAuthor('Karen Brown', 'karen@example.com'); await vcs.add(bazTxt); await vcs.commit('First commit!'); - // Edit file await fsClient.promises.writeFile(bazTxt, 'changedContent'); - // foo is staged, bar is unstaged, but both are untracked (thus, new to git) await vcs.add(`${GIT_INSOMNIA_DIR}/bar.txt`); const fooStatus = await vcs.status(fooTxt); const barStatus = await vcs.status(barTxt); const bazStatus = await vcs.status(bazTxt); - expect(fooStatus).toBe('*added'); expect(barStatus).toBe('added'); expect(bazStatus).toBe('*modified'); - const files: Array = [ - { filePath: fooTxt, status: fooStatus }, - { filePath: barTxt, status: barStatus }, - { filePath: bazTxt, status: bazStatus }, + { + filePath: fooTxt, + status: fooStatus, + }, + { + filePath: barTxt, + status: barStatus, + }, + { + filePath: bazTxt, + status: bazStatus, + }, ]; - // Remove both await gitRollback(vcs, files); - // Ensure git doesn't know about the two files anymore expect(await vcs.status(fooTxt)).toBe('absent'); expect(await vcs.status(barTxt)).toBe('absent'); expect(await vcs.status(bazTxt)).toBe('unmodified'); - // Ensure the two files have been removed from the fs (memClient) await expect(fsClient.promises.readFile(fooTxt)).rejects.toThrowError( `ENOENT: no such file or directory, scandir '${fooTxt}'`, @@ -155,7 +170,6 @@ describe('git rollback', () => { await expect(fsClient.promises.readFile(barTxt)).rejects.toThrowError( `ENOENT: no such file or directory, scandir '${barTxt}'`, ); - expect((await fsClient.promises.readFile(bazTxt)).toString()).toBe(originalContent); }); }); diff --git a/packages/insomnia-app/app/sync/git/__tests__/git-vcs.test.js b/packages/insomnia-app/app/sync/git/__tests__/git-vcs.test.ts similarity index 92% rename from packages/insomnia-app/app/sync/git/__tests__/git-vcs.test.js rename to packages/insomnia-app/app/sync/git/__tests__/git-vcs.test.ts index b503ca028d..8756edb157 100644 --- a/packages/insomnia-app/app/sync/git/__tests__/git-vcs.test.js +++ b/packages/insomnia-app/app/sync/git/__tests__/git-vcs.test.ts @@ -11,22 +11,20 @@ describe('Git-VCS', () => { fooTxt = path.join(GIT_INSOMNIA_DIR, 'foo.txt'); barTxt = path.join(GIT_INSOMNIA_DIR, 'bar.txt'); }); - afterAll(() => jest.restoreAllMocks()); beforeEach(setupDateMocks); - describe('common operations', () => { it('listFiles()', async () => { const fsClient = MemClient.createClient(); - const vcs = new GitVCS(); - await vcs.init({ directory: GIT_CLONE_DIR, fs: fsClient }); + await vcs.init({ + directory: GIT_CLONE_DIR, + fs: fsClient, + }); await vcs.setAuthor('Karen Brown', 'karen@example.com'); - // No files exist yet const files1 = await vcs.listFiles(); expect(files1).toEqual([]); - // File does not exist in git index await fsClient.promises.writeFile('foo.txt', 'bar'); const files2 = await vcs.listFiles(); @@ -38,21 +36,19 @@ describe('Git-VCS', () => { await fsClient.promises.mkdir(GIT_INSOMNIA_DIR); await fsClient.promises.writeFile(fooTxt, 'foo'); await fsClient.promises.writeFile(barTxt, 'bar'); - // Files outside namespace should be ignored await fsClient.promises.writeFile('/other.txt', 'other'); - const vcs = new GitVCS(); - await vcs.init({ directory: GIT_CLONE_DIR, fs: fsClient }); + await vcs.init({ + directory: GIT_CLONE_DIR, + fs: fsClient, + }); await vcs.setAuthor('Karen Brown', 'karen@example.com'); - expect(await vcs.status(barTxt)).toBe('*added'); expect(await vcs.status(fooTxt)).toBe('*added'); - await vcs.add(fooTxt); expect(await vcs.status(barTxt)).toBe('*added'); expect(await vcs.status(fooTxt)).toBe('added'); - await vcs.remove(fooTxt); expect(await vcs.status(barTxt)).toBe('*added'); expect(await vcs.status(fooTxt)).toBe('*added'); @@ -61,7 +57,10 @@ describe('Git-VCS', () => { it('Returns empty log without first commit', async () => { const fsClient = MemClient.createClient(); const vcs = new GitVCS(); - await vcs.init({ directory: GIT_CLONE_DIR, fs: fsClient }); + await vcs.init({ + directory: GIT_CLONE_DIR, + fs: fsClient, + }); await vcs.setAuthor('Karen Brown', 'karen@example.com'); expect(await vcs.log()).toEqual([]); }); @@ -71,18 +70,17 @@ describe('Git-VCS', () => { await fsClient.promises.mkdir(GIT_INSOMNIA_DIR); await fsClient.promises.writeFile(fooTxt, 'foo'); await fsClient.promises.writeFile(barTxt, 'bar'); - await fsClient.promises.writeFile('other.txt', 'should be ignored'); - const vcs = new GitVCS(); - await vcs.init({ directory: GIT_CLONE_DIR, fs: fsClient }); + await vcs.init({ + directory: GIT_CLONE_DIR, + fs: fsClient, + }); await vcs.setAuthor('Karen Brown', 'karen@example.com'); await vcs.add(fooTxt); await vcs.commit('First commit!'); - expect(await vcs.status(barTxt)).toBe('*added'); expect(await vcs.status(fooTxt)).toBe('unmodified'); - expect(await vcs.log()).toEqual([ { commit: { @@ -111,15 +109,12 @@ First commit! `, }, ]); - await fsClient.promises.unlink(fooTxt); expect(await vcs.status(barTxt)).toBe('*added'); expect(await vcs.status(fooTxt)).toBe('*deleted'); - await vcs.remove(fooTxt); expect(await vcs.status(barTxt)).toBe('*added'); expect(await vcs.status(fooTxt)).toBe('deleted'); - await vcs.remove(fooTxt); expect(await vcs.status(barTxt)).toBe('*added'); expect(await vcs.status(fooTxt)).toBe('deleted'); @@ -130,21 +125,20 @@ First commit! await fsClient.promises.mkdir(GIT_INSOMNIA_DIR); await fsClient.promises.writeFile(fooTxt, 'foo'); await fsClient.promises.writeFile(barTxt, 'bar'); - const vcs = new GitVCS(); - await vcs.init({ directory: GIT_CLONE_DIR, fs: fsClient }); + await vcs.init({ + directory: GIT_CLONE_DIR, + fs: fsClient, + }); await vcs.setAuthor('Karen Brown', 'karen@example.com'); await vcs.add(fooTxt); await vcs.commit('First commit!'); - expect((await vcs.log()).length).toBe(1); - await vcs.checkout('new-branch'); expect((await vcs.log()).length).toBe(1); await vcs.add(barTxt); await vcs.commit('Second commit!'); expect((await vcs.log()).length).toBe(2); - await vcs.checkout('master'); expect((await vcs.log()).length).toBe(1); }); @@ -156,7 +150,6 @@ First commit! ok: ['unpack'], errors: ['refs/heads/master pre-receive hook declined'], }); - const vcs = new GitVCS(); await expect(vcs.push()).rejects.toThrowError( 'Push rejected with errors: ["refs/heads/master pre-receive hook declined"].\n\nGo to View > Toggle DevTools > Console for more information.', @@ -169,36 +162,31 @@ First commit! const folder = path.join(GIT_INSOMNIA_DIR, 'folder'); const folderBarTxt = path.join(folder, 'bar.txt'); const originalContent = 'content'; - const fsClient = MemClient.createClient(); await fsClient.promises.mkdir(GIT_INSOMNIA_DIR); await fsClient.promises.writeFile(fooTxt, originalContent); - await fsClient.promises.mkdir(folder); await fsClient.promises.writeFile(folderBarTxt, originalContent); - const vcs = new GitVCS(); - await vcs.init({ directory: GIT_CLONE_DIR, fs: fsClient }); - + await vcs.init({ + directory: GIT_CLONE_DIR, + fs: fsClient, + }); // Commit await vcs.setAuthor('Karen Brown', 'karen@example.com'); await vcs.add(fooTxt); await vcs.add(folderBarTxt); await vcs.commit('First commit!'); - // Change the file await fsClient.promises.writeFile(fooTxt, 'changedContent'); await fsClient.promises.writeFile(folderBarTxt, 'changedContent'); expect(await vcs.status(fooTxt)).toBe('*modified'); expect(await vcs.status(folderBarTxt)).toBe('*modified'); - // Undo await vcs.undoPendingChanges(); - // Ensure git doesn't recognize a change anymore expect(await vcs.status(fooTxt)).toBe('unmodified'); expect(await vcs.status(folderBarTxt)).toBe('unmodified'); - // Expect original doc to have reverted expect((await fsClient.promises.readFile(fooTxt)).toString()).toBe(originalContent); expect((await fsClient.promises.readFile(folderBarTxt)).toString()).toBe(originalContent); @@ -211,34 +199,29 @@ First commit! const files = [foo1Txt, foo2Txt, foo3Txt]; const originalContent = 'content'; const changedContent = 'changedContent'; - const fsClient = MemClient.createClient(); await fsClient.promises.mkdir(GIT_INSOMNIA_DIR); const vcs = new GitVCS(); - await vcs.init({ directory: GIT_CLONE_DIR, fs: fsClient }); - + await vcs.init({ + directory: GIT_CLONE_DIR, + fs: fsClient, + }); // Write to all files await Promise.all(files.map(f => fsClient.promises.writeFile(f, originalContent))); - // Commit all files await vcs.setAuthor('Karen Brown', 'karen@example.com'); await Promise.all(files.map(f => vcs.add(f, originalContent))); await vcs.commit('First commit!'); - // Change all files await Promise.all(files.map(f => fsClient.promises.writeFile(f, changedContent))); - await Promise.all(files.map(f => expect(vcs.status(foo1Txt)).resolves.toBe('*modified'))); - + await Promise.all(files.map(() => expect(vcs.status(foo1Txt)).resolves.toBe('*modified'))); // Undo foo1 and foo2, but not foo3 await vcs.undoPendingChanges([foo1Txt, foo2Txt]); - expect(await vcs.status(foo1Txt)).toBe('unmodified'); expect(await vcs.status(foo2Txt)).toBe('unmodified'); - // Expect original doc to have reverted for foo1 and foo2 expect((await fsClient.promises.readFile(foo1Txt)).toString()).toBe(originalContent); expect((await fsClient.promises.readFile(foo2Txt)).toString()).toBe(originalContent); - // Expect changed content for foo3 expect(await vcs.status(foo3Txt)).toBe('*modified'); expect((await fsClient.promises.readFile(foo3Txt)).toString()).toBe(changedContent); @@ -250,26 +233,23 @@ First commit! const fsClient = MemClient.createClient(); const dir = path.join(GIT_INSOMNIA_DIR, 'dir'); const dirFooTxt = path.join(dir, 'foo.txt'); - await fsClient.promises.mkdir(GIT_INSOMNIA_DIR); await fsClient.promises.mkdir(dir); await fsClient.promises.writeFile(dirFooTxt, 'foo'); - const vcs = new GitVCS(); - await vcs.init({ directory: GIT_CLONE_DIR, fs: fsClient }); + await vcs.init({ + directory: GIT_CLONE_DIR, + fs: fsClient, + }); await vcs.setAuthor('Karen Brown', 'karen@example.com'); - await vcs.add(dirFooTxt); await vcs.commit('First'); - await fsClient.promises.writeFile(dirFooTxt, 'foo bar'); await vcs.add(dirFooTxt); await vcs.commit('Second'); - const log = await vcs.log(); expect(await vcs.readObjFromTree(log[0].commit.tree, dirFooTxt)).toBe('foo bar'); expect(await vcs.readObjFromTree(log[1].commit.tree, dirFooTxt)).toBe('foo'); - // Some extra checks expect(await vcs.readObjFromTree(log[1].commit.tree, 'missing')).toBe(null); expect(await vcs.readObjFromTree('missing', 'missing')).toBe(null); diff --git a/packages/insomnia-app/app/sync/git/__tests__/mem-client.test.js b/packages/insomnia-app/app/sync/git/__tests__/mem-client.test.ts similarity index 85% rename from packages/insomnia-app/app/sync/git/__tests__/mem-client.test.js rename to packages/insomnia-app/app/sync/git/__tests__/mem-client.test.ts index c28151441d..e82a929666 100644 --- a/packages/insomnia-app/app/sync/git/__tests__/mem-client.test.js +++ b/packages/insomnia-app/app/sync/git/__tests__/mem-client.test.ts @@ -6,10 +6,8 @@ import { GIT_CLONE_DIR } from '../git-vcs'; describe('MemClient', () => { afterAll(() => jest.restoreAllMocks()); beforeEach(setupDateMocks); - const fooTxt = 'foo.txt'; const barTxt = 'bar.txt'; - describe('readfile()', () => { it('fails to read', async () => { const fsClient = new MemClient(); @@ -27,14 +25,12 @@ describe('MemClient', () => { it('fails to write over directory', async () => { const fsClient = new MemClient(); const dirName = 'foo'; - await fsClient.mkdir(dirName); await assertAsyncError(fsClient.writeFile(dirName, 'Hello World 2!'), 'EISDIR'); }); it('overwrites file', async () => { const fsClient = new MemClient(); - await fsClient.writeFile(fooTxt, 'Hello World!'); await fsClient.writeFile(fooTxt, 'Hello World 2!'); expect((await fsClient.readFile(fooTxt)).toString()).toBe('Hello World 2!'); @@ -42,36 +38,60 @@ describe('MemClient', () => { it('flag "a" file', async () => { const fsClient = new MemClient(); - - await fsClient.writeFile(fooTxt, 'Hello World!', { flag: 'a' }); - await fsClient.writeFile(fooTxt, 'xxx', { flag: 'a' }); + await fsClient.writeFile(fooTxt, 'Hello World!', { + flag: 'a', + }); + await fsClient.writeFile(fooTxt, 'xxx', { + flag: 'a', + }); expect((await fsClient.readFile(fooTxt)).toString()).toBe('Hello World!xxx'); }); it('flags "ax" and "wx" fail if path exists', async () => { const fsClient = new MemClient(); - await fsClient.writeFile(fooTxt, 'Hello World!'); - await assertAsyncError(fsClient.writeFile(fooTxt, 'aaa', { flag: 'ax' }), 'EEXIST'); - await assertAsyncError(fsClient.writeFile(fooTxt, 'aaa', { flag: 'wx' }), 'EEXIST'); + await assertAsyncError( + fsClient.writeFile(fooTxt, 'aaa', { + flag: 'ax', + }), + 'EEXIST', + ); + await assertAsyncError( + fsClient.writeFile(fooTxt, 'aaa', { + flag: 'wx', + }), + 'EEXIST', + ); }); it('fails if flag "r"', async () => { const fsClient = new MemClient(); - await assertAsyncError(fsClient.writeFile(fooTxt, 'aaa', { flag: 'r' }), 'EBADF'); + await assertAsyncError( + fsClient.writeFile(fooTxt, 'aaa', { + flag: 'r', + }), + 'EBADF', + ); }); it('fails if dir missing', async () => { const fsClient = new MemClient(); - - await assertAsyncError(fsClient.writeFile(fooTxt, 'aaa', { flag: 'r' }), 'EBADF'); + await assertAsyncError( + fsClient.writeFile(fooTxt, 'aaa', { + flag: 'r', + }), + 'EBADF', + ); }); it('works with flags', async () => { const fsClient = new MemClient(); - - await fsClient.writeFile(fooTxt, 'Hello World!', { flag: 'a' }); - await fsClient.writeFile(fooTxt, 'xxx', { flag: 'a' }); + await fsClient.writeFile(fooTxt, 'Hello World!', { + flag: 'a', + }); + await fsClient.writeFile(fooTxt, 'xxx', { + flag: 'a', + }); expect((await fsClient.readFile(fooTxt)).toString()).toBe('Hello World!xxx'); }); }); @@ -79,7 +99,6 @@ describe('MemClient', () => { describe('unlink()', () => { it('unlinks file', async () => { const fsClient = new MemClient(); - await fsClient.writeFile(fooTxt, 'xxx'); await fsClient.unlink(fooTxt); await assertAsyncError(fsClient.readFile(fooTxt), 'ENOENT'); @@ -87,7 +106,6 @@ describe('MemClient', () => { it('fails to unlinks missing file', async () => { const fsClient = new MemClient(); - await assertAsyncError(fsClient.unlink(path.join('not', 'exist.txt')), 'ENOENT'); }); }); @@ -95,10 +113,8 @@ describe('MemClient', () => { describe('readdir()', () => { it('lists dir', async () => { const fsClient = new MemClient(); - // Root dir should always exist expect(await fsClient.readdir(GIT_CLONE_DIR)).toEqual([]); - // Write a file and list it again await fsClient.writeFile(fooTxt, 'Hello World!'); await fsClient.writeFile(barTxt, 'Bar!'); @@ -126,32 +142,31 @@ describe('MemClient', () => { it('creates directory', async () => { const fsClient = new MemClient(); - await fsClient.mkdir(fooDir); await fsClient.mkdir(fooBarDir); - expect(await fsClient.readdir(GIT_CLONE_DIR)).toEqual(['foo']); expect(await fsClient.readdir(cloneFooDir)).toEqual(['bar']); }); it('creates directory non-recursively', async () => { const fsClient = new MemClient(); - - await fsClient.mkdir(cloneFooDir, { recursive: true }); + await fsClient.mkdir(cloneFooDir, { + recursive: true, + }); await fsClient.mkdir(cloneFooBarDir); expect(await fsClient.readdir(cloneFooBarDir)).toEqual([]); }); it('creates directory recursively', async () => { const fsClient = new MemClient(); - - await fsClient.mkdir(cloneFooBarBazDir, { recursive: true }); + await fsClient.mkdir(cloneFooBarBazDir, { + recursive: true, + }); expect(await fsClient.readdir(cloneFooBarBazDir)).toEqual([]); }); it('fails to create if no parent', async () => { const fsClient = new MemClient(); - await assertAsyncError(fsClient.mkdir(cloneFooBarBazDir), 'ENOENT'); }); }); @@ -162,8 +177,9 @@ describe('MemClient', () => { it('removes a dir', async () => { const fsClient = new MemClient(); - - await fsClient.mkdir(abcDir, { recursive: true }); + await fsClient.mkdir(abcDir, { + recursive: true, + }); expect(await fsClient.readdir(abDir)).toEqual(['c']); await fsClient.rmdir(abcDir); expect(await fsClient.readdir(abDir)).toEqual([]); @@ -171,17 +187,16 @@ describe('MemClient', () => { it('fails on non-empty dir', async () => { const fsClient = new MemClient(); - - await fsClient.mkdir(abcDir, { recursive: true }); + await fsClient.mkdir(abcDir, { + recursive: true, + }); await fsClient.writeFile(path.join(abcDir, 'foo.txt'), 'xxx'); - await assertAsyncError(fsClient.rmdir(abDir), 'ENOTEMPTY'); await assertAsyncError(fsClient.rmdir(abcDir), 'ENOTEMPTY'); }); it('fails on file', async () => { const fsClient = new MemClient(); - await fsClient.writeFile(fooTxt, 'xxx'); await assertAsyncError(fsClient.rmdir(fooTxt), 'ENOTDIR'); }); @@ -190,9 +205,7 @@ describe('MemClient', () => { describe('stat()', () => { it('stats root dir', async () => { const fsClient = new MemClient(); - const stat = await fsClient.stat(GIT_CLONE_DIR); - expect(stat).toEqual({ ctimeMs: 1000000000000, mtimeMs: 1000000000000, @@ -204,7 +217,6 @@ describe('MemClient', () => { type: 'dir', uid: 1, }); - expect(stat.isDirectory()).toBe(true); expect(stat.isFile()).toBe(false); expect(stat.isSymbolicLink()).toBe(false); @@ -212,10 +224,8 @@ describe('MemClient', () => { it('stats file', async () => { const fsClient = new MemClient(); - await fsClient.writeFile(fooTxt, 'xxx'); const stat = await fsClient.stat(fooTxt); - expect(stat).toEqual({ ctimeMs: 1000000000001, mtimeMs: 1000000000001, @@ -227,7 +237,6 @@ describe('MemClient', () => { type: 'file', uid: 1, }); - expect(stat.isDirectory()).toBe(false); expect(stat.isFile()).toBe(true); expect(stat.isSymbolicLink()).toBe(false); diff --git a/packages/insomnia-app/app/sync/git/__tests__/ne-db-client.test.js b/packages/insomnia-app/app/sync/git/__tests__/ne-db-client.test.ts similarity index 77% rename from packages/insomnia-app/app/sync/git/__tests__/ne-db-client.test.js rename to packages/insomnia-app/app/sync/git/__tests__/ne-db-client.test.ts index 8634ce5b08..9ee0efcb34 100644 --- a/packages/insomnia-app/app/sync/git/__tests__/ne-db-client.test.js +++ b/packages/insomnia-app/app/sync/git/__tests__/ne-db-client.test.ts @@ -1,8 +1,7 @@ -// @flow import YAML from 'yaml'; import { globalBeforeEach } from '../../../__jest__/before-each'; import * as models from '../../../models'; -import * as db from '../../../common/database'; +import { database as db } from '../../../common/database'; import { assertAsyncError, setupDateMocks } from './util'; import { NeDBClient } from '../ne-db-client'; import path from 'path'; @@ -10,19 +9,27 @@ import { GIT_CLONE_DIR, GIT_INSOMNIA_DIR, GIT_INSOMNIA_DIR_NAME } from '../git-v describe('NeDBClient', () => { afterAll(() => jest.restoreAllMocks()); - beforeEach(async () => { await globalBeforeEach(); - setupDateMocks(); - // Create some sample models - await models.workspace.create({ _id: 'wrk_1' }); - await models.request.create({ _id: 'req_1', parentId: 'wrk_1' }); - await models.request.create({ _id: 'req_2', parentId: 'wrk_1' }); - + await models.workspace.create({ + _id: 'wrk_1', + }); + await models.request.create({ + _id: 'req_1', + parentId: 'wrk_1', + }); + await models.request.create({ + _id: 'req_2', + parentId: 'wrk_1', + }); // Shouldn't list private docs - await models.request.create({ id: 'req_x', isPrivate: true, parentId: 'wrk_1' }); + await models.request.create({ + id: 'req_x', + isPrivate: true, + parentId: 'wrk_1', + }); }); describe('readdir()', () => { @@ -30,7 +37,6 @@ describe('NeDBClient', () => { const neDbClient = new NeDBClient('wrk_1'); const reqDir = path.join(GIT_INSOMNIA_DIR, models.request.type); const wrkDir = path.join(GIT_INSOMNIA_DIR, models.workspace.type); - expect(await neDbClient.readdir(GIT_CLONE_DIR)).toEqual([GIT_INSOMNIA_DIR_NAME]); expect(await neDbClient.readdir(GIT_INSOMNIA_DIR)).toEqual([ models.apiSpec.type, @@ -44,7 +50,6 @@ describe('NeDBClient', () => { models.unitTestSuite.type, models.workspace.type, ]); - expect(await neDbClient.readdir(reqDir)).toEqual(['req_1.yml', 'req_2.yml']); expect(await neDbClient.readdir(wrkDir)).toEqual(['wrk_1.yml']); }); @@ -55,17 +60,19 @@ describe('NeDBClient', () => { const wrk1Yml = path.join(GIT_INSOMNIA_DIR, models.workspace.type, 'wrk_1.yml'); const req1Yml = path.join(GIT_INSOMNIA_DIR, models.request.type, 'req_1.yml'); const reqXYml = path.join(GIT_INSOMNIA_DIR, models.request.type, 'req_x.yml'); - const pNeDB = new NeDBClient('wrk_1'); - expect(YAML.parse(await pNeDB.readFile(wrk1Yml, 'utf8'))).toEqual( - expect.objectContaining({ _id: 'wrk_1', parentId: null }), + expect.objectContaining({ + _id: 'wrk_1', + parentId: null, + }), ); - expect(YAML.parse(await pNeDB.readFile(req1Yml, 'utf8'))).toEqual( - expect.objectContaining({ _id: 'req_1', parentId: 'wrk_1' }), + expect.objectContaining({ + _id: 'req_1', + parentId: 'wrk_1', + }), ); - await assertAsyncError(pNeDB.readFile(reqXYml)); }); }); @@ -75,18 +82,18 @@ describe('NeDBClient', () => { // Assemble const reqDir = path.join(GIT_INSOMNIA_DIR, models.request.type); const wrkDir = path.join(GIT_INSOMNIA_DIR, models.workspace.type); - - const dirType = expect.objectContaining({ type: 'dir' }); - const fileType = expect.objectContaining({ type: 'file' }); - + const dirType = expect.objectContaining({ + type: 'dir', + }); + const fileType = expect.objectContaining({ + type: 'file', + }); // Act const neDbClient = new NeDBClient('wrk_1'); - // Assert expect(await neDbClient.stat(GIT_CLONE_DIR)).toEqual(dirType); expect(await neDbClient.stat(GIT_INSOMNIA_DIR)).toEqual(dirType); expect(await neDbClient.stat(reqDir)).toEqual(dirType); - expect(await neDbClient.stat(path.join(wrkDir, 'wrk_1.yml'))).toEqual(fileType); expect(await neDbClient.stat(path.join(reqDir, 'req_2.yml'))).toEqual(fileType); }); @@ -98,16 +105,16 @@ describe('NeDBClient', () => { const upsertSpy = jest.spyOn(db, 'upsert'); const workspaceId = 'wrk_1'; const neDbClient = new NeDBClient(workspaceId); - - const env = { _id: 'env_1', type: models.environment.type, parentId: workspaceId }; + const env = { + _id: 'env_1', + type: models.environment.type, + parentId: workspaceId, + }; const filePath = path.join('anotherDir', env.type, `${env._id}.yml`); - // Act await neDbClient.writeFile(filePath, YAML.stringify(env)); - // Assert expect(upsertSpy).not.toBeCalled(); - // Cleanup upsertSpy.mockRestore(); }); @@ -117,17 +124,17 @@ describe('NeDBClient', () => { const workspaceId = 'wrk_1'; const neDbClient = new NeDBClient(workspaceId); const upsertSpy = jest.spyOn(db, 'upsert'); - - const env = { _id: 'env_1', type: models.environment.type, parentId: workspaceId }; + const env = { + _id: 'env_1', + type: models.environment.type, + parentId: workspaceId, + }; const filePath = path.join(GIT_INSOMNIA_DIR, env.type, `${env._id}.yml`); - // Act await neDbClient.writeFile(filePath, YAML.stringify(env)); - // Assert expect(upsertSpy).toHaveBeenCalledTimes(1); expect(upsertSpy).toHaveBeenCalledWith(env, true); - // Cleanup upsertSpy.mockRestore(); }); @@ -136,16 +143,17 @@ describe('NeDBClient', () => { // Assemble const workspaceId = 'wrk_1'; const neDbClient = new NeDBClient(workspaceId); - - const env = { _id: 'env_1', type: models.environment.type, parentId: workspaceId }; - const filePath = path.join(GIT_INSOMNIA_DIR, env.type, `env_2.yml`); - + const env = { + _id: 'env_1', + type: models.environment.type, + parentId: workspaceId, + }; + const filePath = path.join(GIT_INSOMNIA_DIR, env.type, 'env_2.yml'); // Act const promiseResult = neDbClient.writeFile(filePath, YAML.stringify(env)); - // Assert await expect(promiseResult).rejects.toThrowError( - `Doc _id does not match file path [env_1 != env_2]`, + 'Doc _id does not match file path [env_1 != env_2]', ); }); @@ -153,16 +161,17 @@ describe('NeDBClient', () => { // Assemble const workspaceId = 'wrk_1'; const neDbClient = new NeDBClient(workspaceId); - - const env = { _id: 'env_1', type: models.environment.type, parentId: workspaceId }; + const env = { + _id: 'env_1', + type: models.environment.type, + parentId: workspaceId, + }; const filePath = path.join(GIT_INSOMNIA_DIR, models.request.type, `${env._id}.yml`); - // Act const promiseResult = neDbClient.writeFile(filePath, YAML.stringify(env)); - // Assert await expect(promiseResult).rejects.toThrowError( - `Doc type does not match file path [Environment != Request]`, + 'Doc type does not match file path [Environment != Request]', ); }); }); @@ -171,9 +180,7 @@ describe('NeDBClient', () => { it('should throw error', async () => { const workspaceId = 'wrk_1'; const neDbClient = new NeDBClient(workspaceId); - const promiseResult = neDbClient.mkdir('', ''); - await expect(promiseResult).rejects.toThrowError('NeDBClient is not writable'); }); }); diff --git a/packages/insomnia-app/app/sync/git/__tests__/parse-git-path.test.js b/packages/insomnia-app/app/sync/git/__tests__/parse-git-path.test.ts similarity index 98% rename from packages/insomnia-app/app/sync/git/__tests__/parse-git-path.test.js rename to packages/insomnia-app/app/sync/git/__tests__/parse-git-path.test.ts index 99725503c3..5f6b53d535 100644 --- a/packages/insomnia-app/app/sync/git/__tests__/parse-git-path.test.js +++ b/packages/insomnia-app/app/sync/git/__tests__/parse-git-path.test.ts @@ -1,5 +1,3 @@ -// @flow - import parseGitPath from '../parse-git-path'; import { GIT_INSOMNIA_DIR } from '../git-vcs'; import * as models from '../../../models'; @@ -8,7 +6,6 @@ describe('parseGitPath', () => { it('should parse a git path into its root, type and id', () => { const gitPath = `${GIT_INSOMNIA_DIR}/${models.workspace.type}/wrk_1.yml`; const result = parseGitPath(gitPath); - expect(result.root).toBe(GIT_INSOMNIA_DIR); expect(result.type).toBe(models.workspace.type); expect(result.id).toBe('wrk_1'); @@ -16,9 +13,7 @@ describe('parseGitPath', () => { it('should ignore multiple slashes', () => { const gitPath = `${GIT_INSOMNIA_DIR}////${models.workspace.type}/wrk_1.yml`; - const result = parseGitPath(gitPath); - expect(result.root).toBe(GIT_INSOMNIA_DIR); expect(result.type).toBe(models.workspace.type); expect(result.id).toBe('wrk_1'); @@ -26,9 +21,7 @@ describe('parseGitPath', () => { it('should ignore current directory . segments', () => { const gitPath = `${GIT_INSOMNIA_DIR}/./././${models.workspace.type}/wrk_1.yml`; - const result = parseGitPath(gitPath); - expect(result.root).toBe(GIT_INSOMNIA_DIR); expect(result.type).toBe(models.workspace.type); expect(result.id).toBe('wrk_1'); @@ -36,9 +29,7 @@ describe('parseGitPath', () => { it.each(['json', 'yml'])('should omit the %s extension', ext => { const gitPath = `${GIT_INSOMNIA_DIR}/${models.workspace.type}/wrk_1.${ext}`; - const result = parseGitPath(gitPath); - expect(result.root).toBe(GIT_INSOMNIA_DIR); expect(result.type).toBe(models.workspace.type); expect(result.id).toBe('wrk_1'); @@ -46,9 +37,7 @@ describe('parseGitPath', () => { it('should keep the extension', () => { const gitPath = `${GIT_INSOMNIA_DIR}/${models.workspace.type}/wrk_1.somethingelse`; - const result = parseGitPath(gitPath); - expect(result.root).toBe(GIT_INSOMNIA_DIR); expect(result.type).toBe(models.workspace.type); expect(result.id).toBe('wrk_1.somethingelse'); diff --git a/packages/insomnia-app/app/sync/git/__tests__/path-sep.test.js b/packages/insomnia-app/app/sync/git/__tests__/path-sep.test.ts similarity index 98% rename from packages/insomnia-app/app/sync/git/__tests__/path-sep.test.js rename to packages/insomnia-app/app/sync/git/__tests__/path-sep.test.ts index 5619c13562..f6cba2f613 100644 --- a/packages/insomnia-app/app/sync/git/__tests__/path-sep.test.js +++ b/packages/insomnia-app/app/sync/git/__tests__/path-sep.test.ts @@ -1,6 +1,6 @@ -// @flow import path from 'path'; import { convertToOsSep, convertToPosixSep } from '../path-sep'; + jest.mock('path'); describe('convertToPosixSep()', () => { @@ -11,7 +11,6 @@ describe('convertToPosixSep()', () => { it.each(['win32', 'posix'])('should convert separator from %s to posix', type => { const input = path[type].join('a', 'b', 'c'); const posix = path.posix.join('a', 'b', 'c'); - expect(convertToPosixSep(input)).toBe(posix); }); }); @@ -23,7 +22,6 @@ describe.each(['win32', 'posix'])('convertToOsSep() where os is %s', osType => { it.each(['win32', 'posix'])(`should convert separators from %s to ${osType}`, inputType => { const input = path[inputType].join('a', 'b', 'c'); const output = path[osType].join('a', 'b', 'c'); - expect(convertToOsSep(input)).toBe(output); }); }); diff --git a/packages/insomnia-app/app/sync/git/__tests__/routable-fs-client.test.js b/packages/insomnia-app/app/sync/git/__tests__/routable-fs-client.test.ts similarity index 92% rename from packages/insomnia-app/app/sync/git/__tests__/routable-fs-client.test.js rename to packages/insomnia-app/app/sync/git/__tests__/routable-fs-client.test.ts index dfcaacb588..1b02c3bddc 100644 --- a/packages/insomnia-app/app/sync/git/__tests__/routable-fs-client.test.js +++ b/packages/insomnia-app/app/sync/git/__tests__/routable-fs-client.test.ts @@ -4,24 +4,21 @@ import { GIT_CLONE_DIR } from '../git-vcs'; describe('routableFSClient', () => { afterAll(() => jest.restoreAllMocks()); + it('routes .git and other files to separate places', async () => { const pGit = MemClient.createClient(); const pDir = MemClient.createClient(); - - const fsClient = routableFSClient(pDir, { '/.git': pGit }).promises; - + const fsClient = routableFSClient(pDir, { + '/.git': pGit, + }).promises; await fsClient.mkdir('/.git'); await fsClient.mkdir('/other'); - await fsClient.writeFile('/other/a.txt', 'a'); await fsClient.writeFile('/.git/b.txt', 'b'); - expect(await pGit.promises.readdir('/.git')).toEqual(['b.txt']); expect(await pDir.promises.readdir('/other')).toEqual(['a.txt']); - // Kind of an edge case, but reading the root dir will not list the .git folder expect(await pDir.promises.readdir(GIT_CLONE_DIR)).toEqual(['other']); - expect((await fsClient.readFile('/other/a.txt')).toString()).toBe('a'); expect((await fsClient.readFile('/.git/b.txt')).toString()).toBe('b'); }); diff --git a/packages/insomnia-app/app/sync/git/__tests__/util.js b/packages/insomnia-app/app/sync/git/__tests__/util.ts similarity index 100% rename from packages/insomnia-app/app/sync/git/__tests__/util.js rename to packages/insomnia-app/app/sync/git/__tests__/util.ts index 0ab6889887..7296911dcf 100644 --- a/packages/insomnia-app/app/sync/git/__tests__/util.js +++ b/packages/insomnia-app/app/sync/git/__tests__/util.ts @@ -21,7 +21,6 @@ export function setupDateMocks() { global.Date = fakeDate; } - export async function assertAsyncError(promise, code) { try { await promise; @@ -30,6 +29,7 @@ export async function assertAsyncError(promise, code) { expect(err.message).toMatch(new RegExp(`^${code}.+`)); expect(err.code).toBe(code); } + return; } diff --git a/packages/insomnia-app/app/sync/git/__tests__/utils.test.js b/packages/insomnia-app/app/sync/git/__tests__/utils.test.ts similarity index 99% rename from packages/insomnia-app/app/sync/git/__tests__/utils.test.js rename to packages/insomnia-app/app/sync/git/__tests__/utils.test.ts index fe70bf5f4a..cb8af4ab08 100644 --- a/packages/insomnia-app/app/sync/git/__tests__/utils.test.js +++ b/packages/insomnia-app/app/sync/git/__tests__/utils.test.ts @@ -1,5 +1,4 @@ import { translateSSHtoHTTP, addDotGit } from '../utils'; - const links = { scp: { bare: 'git@github.com:a/b', diff --git a/packages/insomnia-app/app/sync/git/fs-client.js b/packages/insomnia-app/app/sync/git/fs-client.ts similarity index 72% rename from packages/insomnia-app/app/sync/git/fs-client.js rename to packages/insomnia-app/app/sync/git/fs-client.ts index da5bf9675f..31e86e87e5 100644 --- a/packages/insomnia-app/app/sync/git/fs-client.js +++ b/packages/insomnia-app/app/sync/git/fs-client.ts @@ -1,27 +1,27 @@ -// @flow import fs from 'fs'; import path from 'path'; import mkdirp from 'mkdirp'; type FSWraps = - | fs.FSPromise.readFile - | fs.FSPromise.writeFile - | fs.FSPromise.unlink - | fs.FSPromise.readdir - | fs.FSPromise.mkdir - | fs.FSPromise.rmdir - | fs.FSPromise.stat - | fs.FSPromise.lstat - | fs.FSPromise.readlink - | fs.FSPromise.symlink; + | typeof fs.promises.readFile + | typeof fs.promises.writeFile + | typeof fs.promises.unlink + | typeof fs.promises.readdir + | typeof fs.promises.mkdir + | typeof fs.promises.rmdir + | typeof fs.promises.stat + | typeof fs.promises.lstat + | typeof fs.promises.readlink + | typeof fs.promises.symlink; /** This is a client for isomorphic-git. {@link https://isomorphic-git.org/docs/en/fs} */ export const fsClient = (basePath: string) => { console.log(`[fsClient] Created in ${basePath}`); mkdirp.sync(basePath); - const wrap = (fn: FSWraps) => async (filePath: string, ...args: Array): Promise => { + const wrap = (fn: FSWraps) => async (filePath: string, ...args: Array) => { const modifiedPath = path.join(basePath, path.normalize(filePath)); + // @ts-expect-error -- TSCONVERSION return fn(modifiedPath, ...args); }; diff --git a/packages/insomnia-app/app/sync/git/git-rollback.js b/packages/insomnia-app/app/sync/git/git-rollback.ts similarity index 79% rename from packages/insomnia-app/app/sync/git/git-rollback.js rename to packages/insomnia-app/app/sync/git/git-rollback.ts index 08ab43b1d7..d9a2effdc1 100644 --- a/packages/insomnia-app/app/sync/git/git-rollback.js +++ b/packages/insomnia-app/app/sync/git/git-rollback.ts @@ -1,23 +1,26 @@ -// @flow import { GitVCS } from './git-vcs'; -export type FileWithStatus = { filePath: string, status: string }; +export interface FileWithStatus { + filePath: string; + status: string; +} const isAdded = ({ status }: FileWithStatus) => status.includes('added'); + const isNotAdded = ({ status }: FileWithStatus) => !status.includes('added'); -export const gitRollback = async (vcs: GitVCS, files: Array): Promise => { +export const gitRollback = async (vcs: GitVCS, files: Array) => { const addedFiles = files.filter(isAdded); - // Remove and delete added (unversioned) files - const promises: Array> = addedFiles.map(async ({ filePath }) => { + const promises = addedFiles.map(async ({ filePath }) => { await vcs.remove(filePath); console.log(`[git-rollback] Delete relPath=${filePath}`); + // @ts-expect-error -- TSCONVERSION await vcs.getFs().promises.unlink(filePath); }); - // Rollback existing (versioned) files const existingFiles = files.filter(isNotAdded).map(f => f.filePath); + if (existingFiles.length) { promises.push(vcs.undoPendingChanges(existingFiles)); } diff --git a/packages/insomnia-app/app/sync/git/git-vcs.js b/packages/insomnia-app/app/sync/git/git-vcs.ts similarity index 74% rename from packages/insomnia-app/app/sync/git/git-vcs.js rename to packages/insomnia-app/app/sync/git/git-vcs.ts index 97cbfa05ae..972d3a5c03 100644 --- a/packages/insomnia-app/app/sync/git/git-vcs.js +++ b/packages/insomnia-app/app/sync/git/git-vcs.ts @@ -1,4 +1,3 @@ -// @flow import * as git from 'isomorphic-git'; import { trackEvent } from '../../common/analytics'; import { httpClient } from './http-client'; @@ -6,67 +5,62 @@ import { convertToOsSep, convertToPosixSep } from './path-sep'; import path from 'path'; import { gitCallbacks } from './utils'; -export type GitAuthor = {| - name: string, - email: string, -|}; +export interface GitAuthor { + name: string; + email: string; +} -export type GitRemoteConfig = {| - remote: string, - url: string, -|}; +export interface GitRemoteConfig { + remote: string; + url: string; +} -type GitCredentialsPassword = { - username: string, - password: string, -}; +interface GitCredentialsPassword { + username: string; + password: string; +} -type GitCredentialsToken = { - username: string, - token: string, -}; +interface GitCredentialsToken { + username: string; + token: string; +} export type GitCredentials = GitCredentialsPassword | GitCredentialsToken; export type GitHash = string; + export type GitRef = GitHash | string; -export type GitTimestamp = { - timezoneOffset: number, - timestamp: number, -}; +export interface GitTimestamp { + timezoneOffset: number; + timestamp: number; +} -export type GitLogEntry = {| - oid: string, +export interface GitLogEntry { + oid: string; commit: { - message: string, - tree: GitRef, - author: GitAuthor & GitTimestamp, - committer: GitAuthor & GitTimestamp, - parent: Array, - }, - payload: string, -|}; + message: string; + tree: GitRef; + author: GitAuthor & GitTimestamp; + committer: GitAuthor & GitTimestamp; + parent: Array; + }; + payload: string; +} -export type PushResponse = { - ok?: Array, - errors?: Array, - headers?: object, -}; +interface InitOptions { + directory: string; + fs: git.FsClient; + gitDirectory?: string; +} -type InitOptions = { - directory?: string, - fs?: Object, - gitDirectory?: string, -}; - -type InitFromCloneOptions = { - url: string, - gitCredentials: GitCredentials, - directory: string, - fs: Object, - gitDirectory: string, -}; +interface InitFromCloneOptions { + url: string; + gitCredentials?: GitCredentials | null; + directory: string; + fs: git.FsClient; + gitDirectory: string; +} /** * isomorphic-git internally will default an empty ('') clone directory to '.' @@ -78,21 +72,23 @@ type InitFromCloneOptions = { export const GIT_CLONE_DIR = '.'; const gitInternalDirName = 'git'; export const GIT_INSOMNIA_DIR_NAME = '.insomnia'; - export const GIT_INTERNAL_DIR = path.join(GIT_CLONE_DIR, gitInternalDirName); export const GIT_INSOMNIA_DIR = path.join(GIT_CLONE_DIR, GIT_INSOMNIA_DIR_NAME); +interface BaseOpts { + dir: string; + gitdir?: string; + fs: git.CallbackFsClient | git.PromiseFsClient; + http: git.HttpClient; + onMessage: (message: string) => void; + onAuthFailure: (message: string) => void; + onAuthSuccess: (message: string) => void; + onAuth: () => void; +} + export class GitVCS { - _baseOpts: { - dir: string, - gitdir?: string, - fs: Object, - http: Object, - onMessage: (message: string) => void, - onAuthFailure: (message: string) => void, - onAuthSuccess: (message: string) => void, - onAuth: () => void, - } = gitCallbacks(); + // @ts-expect-error -- TSCONVERSION not initialized with required properties + _baseOpts: BaseOpts = gitCallbacks(); initialized: boolean; @@ -128,31 +124,25 @@ export class GitVCS { fs, http: httpClient, }; - - const cloneParams = { - ...this._baseOpts, - url, - singleBranch: true, - }; + const cloneParams = { ...this._baseOpts, url, singleBranch: true }; await git.clone(cloneParams); - console.log(`[git] Clones repo to ${gitDirectory} from ${url}`); - this.initialized = true; } - isInitialized(): boolean { + isInitialized() { return this.initialized; } - async listFiles(): Promise> { + async listFiles() { console.log('[git] List files'); const files = await git.listFiles({ ...this._baseOpts }); return files.map(convertToOsSep); } - async getBranch(): Promise { + async getBranch() { const branch = await git.currentBranch({ ...this._baseOpts }); + if (typeof branch !== 'string') { throw new Error('No active branch'); } @@ -160,7 +150,7 @@ export class GitVCS { return branch; } - async listBranches(): Promise> { + async listBranches() { const branch = await this.getBranch(); const branches = await git.listBranches({ ...this._baseOpts }); @@ -172,36 +162,29 @@ export class GitVCS { return GitVCS.sortBranches(branches); } - async listRemoteBranches(): Promise> { + async listRemoteBranches() { const branches = await git.listBranches({ ...this._baseOpts, remote: 'origin' }); - // Don't care about returning remote HEAD return GitVCS.sortBranches(branches.filter(b => b !== 'HEAD')); } async status(filepath: string) { - return git.status({ - ...this._baseOpts, - filepath: convertToPosixSep(filepath), - }); + return git.status({ ...this._baseOpts, filepath: convertToPosixSep(filepath) }); } - async add(relPath: string): Promise { + async add(relPath: string) { relPath = convertToPosixSep(relPath); console.log(`[git] Add ${relPath}`); - return git.add({ - ...this._baseOpts, - filepath: relPath, - }); + return git.add({ ...this._baseOpts, filepath: relPath }); } - async remove(relPath: string): Promise { + async remove(relPath: string) { relPath = convertToPosixSep(relPath); console.log(`[git] Remove relPath=${relPath}`); return git.remove({ ...this._baseOpts, filepath: relPath }); } - async addRemote(url: string): Promise { + async addRemote(url: string) { console.log(`[git] Add Remote url=${url}`); await git.addRemote({ ...this._baseOpts, remote: 'origin', url, force: true }); const config = await this.getRemote('origin'); @@ -218,16 +201,16 @@ export class GitVCS { return git.listRemotes({ ...this._baseOpts }); } - async getAuthor(): Promise { + async getAuthor() { const name = await git.getConfig({ ...this._baseOpts, path: 'user.name' }); const email = await git.getConfig({ ...this._baseOpts, path: 'user.email' }); return { name: name || '', email: email || '', - }; + } as GitAuthor; } - async setAuthor(name: string, email: string): Promise { + async setAuthor(name: string, email: string) { await git.setConfig({ ...this._baseOpts, path: 'user.name', value: name }); await git.setConfig({ ...this._baseOpts, path: 'user.email', value: email }); } @@ -237,7 +220,7 @@ export class GitVCS { return remotes.find(r => r.remote === name) || null; } - async commit(message: string): Promise { + async commit(message: string) { console.log(`[git] Commit "${message}"`); trackEvent('Git', 'Commit'); return git.commit({ ...this._baseOpts, message }); @@ -253,8 +236,8 @@ export class GitVCS { */ async canPush(gitCredentials?: GitCredentials | null) { const branch = await this.getBranch(); - const remote = await this.getRemote('origin'); + if (!remote) { throw new Error('Remote not configured'); } @@ -265,12 +248,12 @@ export class GitVCS { forPush: true, url: remote.url, }); - const logs = (await this.log(1)) || []; const localHead = logs[0].oid; const remoteRefs = remoteInfo.refs || {}; const remoteHeads = remoteRefs.heads || {}; const remoteHead = remoteHeads[branch]; + if (localHead === remoteHead) { return false; } @@ -278,20 +261,21 @@ export class GitVCS { return true; } - async push(gitCredentials?: GitCredentials | null, force: boolean = false): Promise { + async push(gitCredentials?: GitCredentials | null, force = false) { console.log(`[git] Push remote=origin force=${force ? 'true' : 'false'}`); trackEvent('Git', 'Push'); - // eslint-disable-next-line no-unreachable - const response: PushResponse = await git.push({ + const response: git.PushResult = await git.push({ ...this._baseOpts, ...gitCallbacks(gitCredentials), remote: 'origin', force, }); + // @ts-expect-error -- TSCONVERSION git errors are not handled correctly if (response.errors?.length) { - console.log(`[git] Push rejected`, response); + console.log('[git] Push rejected', response); + // @ts-expect-error -- TSCONVERSION git errors are not handled correctly const errorsString = JSON.stringify(response.errors); throw new Error( `Push rejected with errors: ${errorsString}.\n\nGo to View > Toggle DevTools > Console for more information.`, @@ -299,33 +283,34 @@ export class GitVCS { } } - async pull(gitCredentials?: GitCredentials | null): Promise { + async pull(gitCredentials?: GitCredentials | null) { console.log('[git] Pull remote=origin', await this.getBranch()); trackEvent('Git', 'Pull'); - return git.pull({ ...this._baseOpts, ...gitCallbacks(gitCredentials), remote: 'origin', singleBranch: true, - fast: true, }); } - async merge(theirBranch: string): Promise { + async merge(theirBranch: string) { const ours = await this.getBranch(); console.log(`[git] Merge ${ours} <-- ${theirBranch}`); trackEvent('Git', 'Merge'); - return git.merge({ ...this._baseOpts, ours, theirs: theirBranch }); + return git.merge({ + ...this._baseOpts, + ours, + theirs: theirBranch, + }); } async fetch( singleBranch: boolean, - depth: number | null, + depth: number, gitCredentials?: GitCredentials | null, - ): Promise { + ) { console.log('[git] Fetch remote=origin'); - return git.fetch({ ...this._baseOpts, ...gitCallbacks(gitCredentials), @@ -337,29 +322,33 @@ export class GitVCS { }); } - async log(depth?: number): Promise> { + async log(depth?: number) { try { return await git.log({ ...this._baseOpts, depth }); } catch (error) { if (error.code === 'NotFoundError') { return []; } + throw error; } } - async branch(branch: string, checkout: boolean = false): Promise { + async branch(branch: string, checkout = false) { trackEvent('Git', 'Create Branch'); + // @ts-expect-error -- TSCONVERSION remote doesn't exist as an option await git.branch({ ...this._baseOpts, ref: branch, checkout, remote: 'origin' }); } - async deleteBranch(branch: string): Promise { + async deleteBranch(branch: string) { trackEvent('Git', 'Delete Branch'); await git.deleteBranch({ ...this._baseOpts, ref: branch }); } - async checkout(branch: string): Promise { - console.log('[git] Checkout', { branch }); + async checkout(branch: string) { + console.log('[git] Checkout', { + branch, + }); const branches = await this.listBranches(); if (branches.includes(branch)) { @@ -370,9 +359,8 @@ export class GitVCS { } } - async undoPendingChanges(fileFilter?: Array): Promise { + async undoPendingChanges(fileFilter?: Array) { console.log('[git] Undo pending changes'); - await git.checkout({ ...this._baseOpts, ref: await this.getBranch(), @@ -382,7 +370,7 @@ export class GitVCS { }); } - async readObjFromTree(treeOid: string, objPath: string): Object | null { + async readObjFromTree(treeOid: string, objPath: string) { try { const obj = await git.readObject({ ...this._baseOpts, @@ -390,7 +378,6 @@ export class GitVCS { filepath: convertToPosixSep(objPath), encoding: 'utf8', }); - return obj.object; } catch (err) { return null; diff --git a/packages/insomnia-app/app/sync/git/http-client.js b/packages/insomnia-app/app/sync/git/http-client.ts similarity index 95% rename from packages/insomnia-app/app/sync/git/http-client.js rename to packages/insomnia-app/app/sync/git/http-client.ts index 5772ce894d..eccf20a7f7 100644 --- a/packages/insomnia-app/app/sync/git/http-client.js +++ b/packages/insomnia-app/app/sync/git/http-client.ts @@ -4,8 +4,8 @@ import { axiosRequest } from '../../network/axios-request'; export const httpClient = { request: async config => { let response; + let body: Buffer | null = null; - let body = null; if (Array.isArray(config.body)) { body = Buffer.concat(config.body); } diff --git a/packages/insomnia-app/app/sync/git/mem-client.js b/packages/insomnia-app/app/sync/git/mem-client.ts similarity index 69% rename from packages/insomnia-app/app/sync/git/mem-client.js rename to packages/insomnia-app/app/sync/git/mem-client.ts index ea7db03fd1..0aa6766315 100644 --- a/packages/insomnia-app/app/sync/git/mem-client.js +++ b/packages/insomnia-app/app/sync/git/mem-client.ts @@ -1,33 +1,34 @@ -// @flow import path from 'path'; import Stat from './stat'; +import { SystemError } from './system-error'; +import { BufferEncoding } from './utils'; -type FSFile = {| - +type: 'file', - +ino: number, - +mtimeMs: number, - +name: string, - +path: string, - contents: string, -|}; +interface FSFile { + readonly type: 'file'; + readonly ino: number; + readonly mtimeMs: number; + readonly name: string; + readonly path: string; + contents: string; +} -type FSLink = {| - +type: 'symlink', - +ino: number, - +mtimeMs: number, - +name: string, - +path: string, - +linkTo: string, -|}; +interface FSLink { + readonly type: 'symlink'; + readonly ino: number; + readonly mtimeMs: number; + readonly name: string; + readonly path: string; + readonly linkTo: string; +} -type FSDir = {| - +type: 'dir', - +ino: number, - +mtimeMs: number, - +name: string, - +path: string, - +children: Array, -|}; +interface FSDir { + readonly type: 'dir'; + readonly ino: number; + readonly mtimeMs: number; + readonly name: string; + readonly path: string; + readonly children: Array; +} type FSEntry = FSDir | FSFile | FSLink; @@ -53,10 +54,10 @@ export class MemClient { }; } - async tree(baseDir: string = '/') { + async tree(baseDir = '/') { baseDir = path.normalize(baseDir); - const next = async (dir: string, toPrint: string): Promise => { + const next = async (dir: string, toPrint: string) => { const entry = this._find(dir); if (!entry) { @@ -64,6 +65,7 @@ export class MemClient { } const indent = new Array((dir.match(/\//g) || []).length).join('| '); + if (entry.type === 'dir') { if (entry.path !== baseDir) { toPrint += `${indent}${entry.name}/\n`; @@ -84,18 +86,22 @@ export class MemClient { async readFile( filePath: string, - options?: buffer$Encoding | { encoding?: buffer$Encoding } = {}, - ): Promise { + options: BufferEncoding | { encoding?: BufferEncoding } = {}, + ) { filePath = path.normalize(filePath); if (typeof options === 'string') { - options = { encoding: options }; + options = { + encoding: options, + }; } const encoding = options ? options.encoding : null; const entry = this._assertFile(filePath); + const raw = Buffer.from(entry.contents, 'base64'); + if (encoding) { return raw.toString(encoding); } else { @@ -106,12 +112,14 @@ export class MemClient { async writeFile( filePath: string, data: Buffer | string, - options: buffer$Encoding | { encoding?: buffer$Encoding, flag?: string }, - ): Promise { + options: BufferEncoding | { encoding?: BufferEncoding, flag?: string; } = {}, + ) { filePath = path.normalize(filePath); if (typeof options === 'string') { - options = { encoding: options }; + options = { + encoding: options, + }; } const flag = options && options.flag ? options.flag : 'w'; @@ -143,32 +151,35 @@ export class MemClient { const dataBuff: Buffer = data instanceof Buffer ? data : Buffer.from(data, encoding); let newContents = Buffer.alloc(0); + if (flag[0] === 'w') { newContents = dataBuff; } else if (flag[0] === 'a') { const contentsBuff: Buffer = Buffer.from(file.contents, 'base64'); newContents = Buffer.concat([contentsBuff, dataBuff]); } else { - const e: ErrnoError = new Error('EBADF: bad file descriptor, write'); - e.errno = -9; - e.code = 'EBADF'; - e.syscall = 'write'; - e.path = filePath; - throw e; + throw new SystemError({ + code: 'EBADF', + errno: -9, + message: 'EBADF: bad file descriptor, write', + path: filePath, + syscall: 'write', + }); } file.contents = newContents.toString('base64'); - return Promise.resolve(); } - async unlink(filePath: string, ...x: Array): Promise { + async unlink(filePath: string) { filePath = path.normalize(filePath); + this._remove(this._assertFile(filePath)); } - async readdir(basePath: string, ...x: Array): Promise> { + async readdir(basePath: string) { basePath = path.normalize(basePath); + const entry = this._assertDir(basePath); const names = entry.children.map(c => c.name); @@ -176,9 +187,8 @@ export class MemClient { return names; } - async mkdir(dirPath: string, options?: { recursive?: boolean }): Promise { + async mkdir(dirPath: string, options?: { recursive?: boolean }) { dirPath = path.normalize(dirPath); - const doRecursive = (options || {}).recursive || false; // If not recursive, ensure parent exists @@ -187,12 +197,13 @@ export class MemClient { } const pathSegments = dirPath.split(path.sep).filter(s => s !== ''); - // Recurse over all sub paths, ensure they are all directories, // create them if they don't exist let currentPath = '/'; + for (const pathSegment of pathSegments) { const dirEntry = this._assertDir(currentPath); + const nextPath = path.join(currentPath, pathSegment); // Create dir if it doesn't exist yet @@ -211,47 +222,54 @@ export class MemClient { } } - async rmdir(dirPath: string, ...x: Array) { + async rmdir(dirPath: string) { dirPath = path.normalize(dirPath); const dirEntry = this._assertDir(dirPath); if (dirEntry.children.length > 0) { - const e: ErrnoError = new Error(`ENOTEMPTY: directory not empty, rmdir '${dirPath}'`); - e.errno = -66; - e.syscall = 'rmdir'; - e.code = 'ENOTEMPTY'; - e.path = dirPath; - throw e; + throw new SystemError({ + code: 'ENOTEMPTY', + errno: -66, + message: `ENOTEMPTY: directory not empty, rmdir '${dirPath}'`, + path: dirPath, + syscall: 'rmdir', + }); } this._remove(dirEntry); } - async stat(filePath: string, ...x: Array): Promise { + async stat(filePath: string) { filePath = path.normalize(filePath); return this._statEntry(this._assertExists(filePath)); } - async lstat(filePath: string, ...x: Array) { + async lstat(filePath: string) { filePath = path.normalize(filePath); + const linkEntry = this._assertExists(filePath); + return this._statEntry(this._resolveLinks(linkEntry)); } - async readlink(filePath: string, ...x: Array) { + async readlink(filePath: string) { filePath = path.normalize(filePath); + const linkEntry = this._assertSymlink(filePath); + return linkEntry.linkTo; } - async symlink(target: string, filePath: string, ...x: Array) { + async symlink(target: string, filePath: string) { filePath = path.normalize(filePath); + // Make sure we don't already have one there // TODO: Check what to do in this case (might be wrong) this._assertDoesNotExist(filePath); this._assertExists(target); + const parentEntry = this._assertDir(path.dirname(filePath)); parentEntry.children.push({ @@ -264,24 +282,25 @@ export class MemClient { }); } - _statEntry(entry: FSEntry): Stat { + _statEntry(entry: FSEntry) { return new Stat({ type: entry.type, mode: 0o777, + // @ts-expect-error -- TSCONVERSION size: entry.contents ? entry.contents.length : 0, ino: entry.ino, mtimeMs: entry.mtimeMs, }); } - _find(filePath: string): FSEntry | null { + _find(filePath: string) { filePath = path.normalize(filePath); - let current = this.__fs; - // Ignore empty and current directory '.' segments const pathSegments = filePath.split(path.sep).filter(s => s !== '' && s !== '.'); + for (const expectedName of pathSegments) { + // @ts-expect-error -- TSCONVERSION const e = (current.children || []).find(c => c.name === expectedName); if (!e) { @@ -295,73 +314,80 @@ export class MemClient { return current; } - _assertDoesNotExist(filePath: string): void { + _assertDoesNotExist(filePath: string) { const entry = this._find(filePath); if (entry) { - const e: ErrnoError = new Error(`EEXIST: file already exists, open '${filePath}'`); - e.errno = -17; - e.code = 'EEXIST'; - e.syscall = 'open'; - e.path = filePath; - throw e; + throw new SystemError({ + code: 'EEXIST', + errno: -17, + message: `EEXIST: file already exists, open '${filePath}'`, + path: filePath, + syscall: 'open', + }); } } - _assertExists(filePath: string): FSEntry { + _assertExists(filePath: string) { const entry = this._find(filePath); if (!entry) { - const e: ErrnoError = new Error(`ENOENT: no such file or directory, scandir '${filePath}'`); - e.errno = -2; - e.code = 'ENOENT'; - e.syscall = 'scandir'; - e.path = filePath; - throw e; + throw new SystemError({ + code: 'ENOENT', + errno: -2, + message: `ENOENT: no such file or directory, scandir '${filePath}'`, + path: filePath, + syscall: 'scandir', + }); } return entry; } - _assertDirEntry(entry: FSEntry): FSDir { + _assertDirEntry(entry: FSEntry) { if (entry.type !== 'dir') { - const e: ErrnoError = new Error(`ENOTDIR: not a directory, scandir '${entry.path}'`); - e.errno = -20; - e.code = 'ENOTDIR'; - e.syscall = 'scandir'; - e.path = entry.path; - throw e; + throw new SystemError({ + code: 'ENOTDIR', + errno: -20, + message: `ENOTDIR: not a directory, scandir '${entry.path}'`, + path: entry.path, + syscall: 'scandir', + }); } return entry; } - _assertDir(filePath: string): FSDir { + _assertDir(filePath: string) { const entry = this._assertExists(filePath); + return this._assertDirEntry(entry); } - _assertSymlinkEntry(entry: FSEntry): FSLink { + _assertSymlinkEntry(entry: FSEntry) { if (entry.type !== 'symlink') { - const e: ErrnoError = new Error(`ENOTDIR: not a simlink, scandir '${entry.path}'`); - e.errno = -20; - e.code = 'ENOTDIR'; - e.syscall = 'scandir'; - e.path = entry.path; - throw e; + throw new SystemError({ + code: 'ENOTDIR', + errno: -20, + message: `ENOTDIR: not a simlink, scandir '${entry.path}'`, + path: entry.path, + syscall: 'scandir', + }); } return entry; } - _assertSymlink(filePath: string): FSLink { + _assertSymlink(filePath: string) { const entry = this._assertExists(filePath); + return this._assertSymlinkEntry(entry); } _resolveLinks(entry: FSEntry): FSFile | FSDir { if (entry.type === 'symlink') { const other = this._find(entry.linkTo); + if (!other) { // Should never happen throw new Error('Failed to resolve link'); @@ -373,29 +399,33 @@ export class MemClient { return entry; } - _assertFileEntry(entry: FSEntry): FSFile { + _assertFileEntry(entry: FSEntry) { entry = this._resolveLinks(entry); if (entry.type === 'dir') { - const e: ErrnoError = new Error(`EISDIR: illegal operation on a directory '${entry.path}'`); - e.errno = -21; - e.code = 'EISDIR'; - e.syscall = 'open'; - e.path = entry.path; - throw e; + throw new SystemError({ + code: 'EISDIR', + errno: -21, + message: `EISDIR: illegal operation on a directory '${entry.path}'`, + path: entry.path, + syscall: 'open', + }); } return entry; } - _assertFile(filePath: string): FSFile { + _assertFile(filePath: string) { const entry = this._assertExists(filePath); + return this._assertFileEntry(entry); } _remove(entry: FSEntry) { const parentEntry = this._assertDir(path.dirname(entry.path)); + const index = parentEntry.children.findIndex(c => c === entry); + if (index < 0) { // Should never happen so w/e return; diff --git a/packages/insomnia-app/app/sync/git/ne-db-client.js b/packages/insomnia-app/app/sync/git/ne-db-client.ts similarity index 73% rename from packages/insomnia-app/app/sync/git/ne-db-client.js rename to packages/insomnia-app/app/sync/git/ne-db-client.ts index d7332c6859..097f37bbd0 100644 --- a/packages/insomnia-app/app/sync/git/ne-db-client.js +++ b/packages/insomnia-app/app/sync/git/ne-db-client.ts @@ -1,11 +1,12 @@ -// @flow import path from 'path'; -import * as db from '../../common/database'; +import { database as db } from '../../common/database'; import * as models from '../../models'; import YAML from 'yaml'; import Stat from './stat'; import { GIT_INSOMNIA_DIR_NAME } from './git-vcs'; import parseGitPath from './parse-git-path'; +import { BufferEncoding } from './utils'; +import { SystemError } from './system-error'; export class NeDBClient { _workspaceId: string; @@ -14,6 +15,7 @@ export class NeDBClient { if (!workspaceId) { throw new Error('Cannot use NeDBClient without workspace ID'); } + this._workspaceId = workspaceId; } @@ -25,13 +27,15 @@ export class NeDBClient { async readFile( filePath: string, - options?: buffer$Encoding | { encoding?: buffer$Encoding }, - ): Promise { + options?: BufferEncoding | { encoding?: BufferEncoding }, + ) { filePath = path.normalize(filePath); - options = options || {}; + if (typeof options === 'string') { - options = { encoding: options }; + options = { + encoding: options, + }; } const { root, type, id } = parseGitPath(filePath); @@ -42,7 +46,7 @@ export class NeDBClient { const doc = await db.get(type, id); - if (!doc || (doc: any).isPrivate) { + if (!doc || doc.isPrivate) { throw this._errMissing(filePath); } @@ -56,7 +60,6 @@ export class NeDBClient { // throw new Error(`Not found under workspace ${filePath}`); // } // } - const raw = Buffer.from(YAML.stringify(doc), 'utf8'); if (options.encoding) { @@ -66,7 +69,7 @@ export class NeDBClient { } } - async writeFile(filePath: string, data: Buffer | string, ...x: Array): Promise { + async writeFile(filePath: string, data: Buffer | string) { filePath = path.normalize(filePath); const { root, id, type } = parseGitPath(filePath); @@ -88,7 +91,7 @@ export class NeDBClient { await db.upsert(doc, true); } - async unlink(filePath: string, ...x: Array): Promise { + async unlink(filePath: string) { filePath = path.normalize(filePath); const { id, type } = parseGitPath(filePath); @@ -105,50 +108,61 @@ export class NeDBClient { await db.unsafeRemove(doc, true); } - async readdir(filePath: string, ...x: Array): Promise> { + async readdir(filePath: string) { filePath = path.normalize(filePath); - const { root, type, id } = parseGitPath(filePath); - let docs = []; let otherFolders = []; + if (root === null && id === null && type === null) { + // @ts-expect-error -- TSCONVERSION otherFolders = [GIT_INSOMNIA_DIR_NAME]; } else if (id === null && type === null) { otherFolders = [ + // @ts-expect-error -- TSCONVERSION models.workspace.type, + // @ts-expect-error -- TSCONVERSION models.environment.type, + // @ts-expect-error -- TSCONVERSION models.requestGroup.type, + // @ts-expect-error -- TSCONVERSION models.request.type, + // @ts-expect-error -- TSCONVERSION models.apiSpec.type, + // @ts-expect-error -- TSCONVERSION models.unitTestSuite.type, + // @ts-expect-error -- TSCONVERSION models.unitTest.type, + // @ts-expect-error -- TSCONVERSION models.grpcRequest.type, + // @ts-expect-error -- TSCONVERSION models.protoFile.type, + // @ts-expect-error -- TSCONVERSION models.protoDirectory.type, ]; } else if (type !== null && id === null) { const workspace = await db.get(models.workspace.type, this._workspaceId); const children = await db.withDescendants(workspace); - docs = children.filter(d => d.type === type && !(d: any).isPrivate); + // @ts-expect-error -- TSCONVERSION + docs = children.filter(d => d.type === type && !d.isPrivate); } else { throw this._errMissing(filePath); } + // @ts-expect-error -- TSCONVERSION const ids = docs.map(d => `${d._id}.yml`); - return [...ids, ...otherFolders].sort(); } - async mkdir(filePath: string, ...x: Array) { + async mkdir() { throw new Error('NeDBClient is not writable'); } - async stat(filePath: string, ...x: Array): Promise { + async stat(filePath: string) { filePath = path.normalize(filePath); - let fileBuff: Buffer | string | null = null; let dir: Array | null = null; + try { fileBuff = await this.readFile(filePath); } catch (err) { @@ -173,7 +187,8 @@ export class NeDBClient { type: 'file', mode: 0o777, size: fileBuff.length, - ino: doc._id, // should be number instead of string https://nodejs.org/api/fs.html#fs_stats_ino I think flow should have detected this + ino: doc._id, + // should be number instead of string https://nodejs.org/api/fs.html#fs_stats_ino I think flow should have detected this mtimeMs: doc.modified, }); } else { @@ -187,29 +202,30 @@ export class NeDBClient { } } - async readlink(filePath: string, ...x: Array): Promise { + async readlink(filePath: string, ...x: Array) { return this.readFile(filePath, ...x); } - async lstat(filePath: string, ...x: Array): Promise { - return this.stat(filePath, ...x); + async lstat(filePath: string) { + return this.stat(filePath); } - async rmdir(dir: string, ...x: Array): Promise { + async rmdir() { // Dirs in NeDB can't be removed, so we'll just pretend like it succeeded return Promise.resolve(); } - async symlink(targetPath: string, filePath: string, ...x: Array): Promise { + async symlink() { throw new Error('NeDBClient symlink not supported'); } - _errMissing(filePath: string): Error { - const e: ErrnoError = new Error(`ENOENT: no such file or directory, scandir '${filePath}'`); - e.errno = -2; - e.code = 'ENOENT'; - e.syscall = 'scandir'; - e.path = filePath; - return e; + _errMissing(filePath: string) { + return new SystemError({ + message: `ENOENT: no such file or directory, scandir '${filePath}'`, + errno: -2, + code: 'ENOENT', + syscall: 'scandir', + path: filePath, + }); } } diff --git a/packages/insomnia-app/app/sync/git/parse-git-path.js b/packages/insomnia-app/app/sync/git/parse-git-path.ts similarity index 89% rename from packages/insomnia-app/app/sync/git/parse-git-path.js rename to packages/insomnia-app/app/sync/git/parse-git-path.ts index 822f24e84a..c255bd2ad9 100644 --- a/packages/insomnia-app/app/sync/git/parse-git-path.js +++ b/packages/insomnia-app/app/sync/git/parse-git-path.ts @@ -1,30 +1,25 @@ -// @flow - import path from 'path'; import { GIT_CLONE_DIR } from './git-vcs'; // The win32 separator is a single backslash (\), but we have to escape both the JS string and RegExp. const pathSep = path.sep === path.win32.sep ? '\\\\' : '/'; + const _cloneDirRegExp = new RegExp(`^${GIT_CLONE_DIR}${pathSep}`); -type GitPathSegments = { - root: string | null, - type: string | null, - id: string | null, -}; +interface GitPathSegments { + root: string | null; + type: string | null; + id: string | null; +} const parseGitPath = (filePath: string): GitPathSegments => { filePath = path.normalize(filePath); - // FilePath will start with the clone directory. We want to remove the clone dir, so that the // segments can be extracted correctly. filePath = filePath.replace(_cloneDirRegExp, ''); - // Ignore empty and current directory '.' segments const [root, type, idRaw] = filePath.split(path.sep).filter(s => s !== '' && s !== '.'); - const id = typeof idRaw === 'string' ? idRaw.replace(/\.(json|yml)$/, '') : idRaw; - return { root: root || null, type: type || null, diff --git a/packages/insomnia-app/app/sync/git/path-sep.js b/packages/insomnia-app/app/sync/git/path-sep.ts similarity index 98% rename from packages/insomnia-app/app/sync/git/path-sep.js rename to packages/insomnia-app/app/sync/git/path-sep.ts index 9b962ccca3..42c822cb84 100644 --- a/packages/insomnia-app/app/sync/git/path-sep.js +++ b/packages/insomnia-app/app/sync/git/path-sep.ts @@ -1,4 +1,3 @@ -// @flow import path from 'path'; const win32SepRegex = /\\/g; diff --git a/packages/insomnia-app/app/sync/git/routable-fs-client.js b/packages/insomnia-app/app/sync/git/routable-fs-client.ts similarity index 84% rename from packages/insomnia-app/app/sync/git/routable-fs-client.js rename to packages/insomnia-app/app/sync/git/routable-fs-client.ts index 0e241c21b2..af70e8c4db 100644 --- a/packages/insomnia-app/app/sync/git/routable-fs-client.js +++ b/packages/insomnia-app/app/sync/git/routable-fs-client.ts @@ -1,5 +1,5 @@ -// @flow import path from 'path'; +import * as git from 'isomorphic-git'; /** * An isometric-git FS client that can route to various client depending on what the filePath is. @@ -8,7 +8,10 @@ import path from 'path'; * @param otherFS – map of path prefixes to clients * @returns {{promises: *}} */ -export function routableFSClient(defaultFS: Object, otherFS: { [string]: Object }) { +export function routableFSClient( + defaultFS: git.PromiseFsClient, + otherFS: Record, +) { const execMethod = async (method: string, filePath: string, ...args: Array) => { filePath = path.normalize(filePath); @@ -20,18 +23,15 @@ export function routableFSClient(defaultFS: Object, otherFS: { [string]: Object // Uncomment this to debug operations // console.log('[routablefs] Executing', method, filePath, { args }); - // Fallback to default if no prefix matched const result = await defaultFS.promises[method](filePath, ...args); - // Uncomment this to debug operations // console.log('[routablefs] Executing', method, filePath, { args }, { result }); - return result; }; - const methods = {}; - + // @ts-expect-error -- TSCONVERSION declare and initialize together to avoid an error + const methods: git.CallbackFsClient = {}; methods.readFile = execMethod.bind(methods, 'readFile'); methods.writeFile = execMethod.bind(methods, 'writeFile'); methods.unlink = execMethod.bind(methods, 'unlink'); @@ -42,7 +42,6 @@ export function routableFSClient(defaultFS: Object, otherFS: { [string]: Object methods.lstat = execMethod.bind(methods, 'lstat'); methods.readlink = execMethod.bind(methods, 'readlink'); methods.symlink = execMethod.bind(methods, 'symlink'); - return { promises: methods, }; diff --git a/packages/insomnia-app/app/sync/git/shallow-clone.js b/packages/insomnia-app/app/sync/git/shallow-clone.ts similarity index 80% rename from packages/insomnia-app/app/sync/git/shallow-clone.js rename to packages/insomnia-app/app/sync/git/shallow-clone.ts index 25d2232e58..ad10332c77 100644 --- a/packages/insomnia-app/app/sync/git/shallow-clone.js +++ b/packages/insomnia-app/app/sync/git/shallow-clone.ts @@ -1,21 +1,19 @@ -// @flow - import * as git from 'isomorphic-git'; import { GIT_CLONE_DIR, GIT_INTERNAL_DIR } from './git-vcs'; import type { GitRepository } from '../../models/git-repository'; import { gitCallbacks } from './utils'; import { httpClient } from './http-client'; -type Options = { - fsClient: Object, - gitRepository: GitRepository, -}; +interface Options { + fsClient: git.FsClient; + gitRepository: GitRepository; +} /** * Create a shallow clone into the provided FS plugin. * */ export const shallowClone = async ({ fsClient, gitRepository }: Options) => { - const cloneParams = { + await git.clone({ ...gitCallbacks(gitRepository.credentials), fs: fsClient, http: httpClient, @@ -24,7 +22,5 @@ export const shallowClone = async ({ fsClient, gitRepository }: Options) => { singleBranch: true, url: gitRepository.uri, depth: 1, - }; - - await git.clone(cloneParams); + }); }; diff --git a/packages/insomnia-app/app/sync/git/stat.js b/packages/insomnia-app/app/sync/git/stat.ts similarity index 80% rename from packages/insomnia-app/app/sync/git/stat.js rename to packages/insomnia-app/app/sync/git/stat.ts index 6785e702b5..c45f866026 100644 --- a/packages/insomnia-app/app/sync/git/stat.js +++ b/packages/insomnia-app/app/sync/git/stat.ts @@ -1,13 +1,11 @@ -// @flow - -type StatObj = { - type: 'file' | 'dir' | 'symlink', - mode: number, - size: number, - ino: number, - mtimeMs: number, - ctimeMs?: number, -}; +interface StatObj { + type: 'file' | 'dir' | 'symlink'; + mode: number; + size: number; + ino: number; + mtimeMs: number; + ctimeMs?: number; +} export default class Stat { type: 'file' | 'dir' | 'symlink'; diff --git a/packages/insomnia-app/app/sync/git/system-error.ts b/packages/insomnia-app/app/sync/git/system-error.ts new file mode 100644 index 0000000000..0237563c22 --- /dev/null +++ b/packages/insomnia-app/app/sync/git/system-error.ts @@ -0,0 +1,46 @@ +interface SystemErrorConstructor { + /** The string error code */ + code: 'EBADF' | 'ENOTEMPTY' | 'EEXIST' | 'ENOENT' | 'ENOTDIR' | 'EISDIR'; + + /** The system-provided error number */ + errno: -9 | -66 | -17 | -2 | -20 | -21; + + message: string; + + /** If present, the file path when reporting a file system error */ + path: string; + + /** The name of the system call that triggered the error */ + syscall: 'write' | 'rmdir' | 'open' | 'scandir'; +} + +/** + * This is basically Node's SystemError, which Node (intentionally) does not expose to prevent people from doing... exactly what we're doing with it (which is throw a SystemError even though we're not System). + * see https://nodejs.org/api/errors.html#errors_class_systemerror + */ +export class SystemError extends Error { + code: SystemErrorConstructor['code']; + errno: SystemErrorConstructor['errno']; + path: SystemErrorConstructor['path']; + syscall: SystemErrorConstructor['syscall']; + + constructor({ + code, + errno, + message, + path, + syscall, + }: SystemErrorConstructor) { + super(message); + + const error = new Error(message); + this.message = error.message; + this.name = error.name; + this.stack = error.stack; + + this.code = code; + this.errno = errno; + this.path = path; + this.syscall = syscall; + } +} diff --git a/packages/insomnia-app/app/sync/git/utils.js b/packages/insomnia-app/app/sync/git/utils.ts similarity index 57% rename from packages/insomnia-app/app/sync/git/utils.js rename to packages/insomnia-app/app/sync/git/utils.ts index 2518d8d81b..3ebdaa382a 100644 --- a/packages/insomnia-app/app/sync/git/utils.js +++ b/packages/insomnia-app/app/sync/git/utils.ts @@ -1,4 +1,3 @@ -// @flow import type { GitCredentials } from './git-vcs'; export const translateSSHtoHTTP = (url: string) => { @@ -23,14 +22,19 @@ const onAuthSuccess = (message: string) => { console.log(`[git-event] Auth Success: ${message}`); }; -const onAuth = (credentials: GitCredentials = {}) => () => ({ +// @ts-expect-error -- TSCONVERSION this needs to be handled better if credentials is undefined or which union type +const onAuth = (credentials?: GitCredentials = {}) => () => ({ username: credentials.username, +// @ts-expect-error -- TSCONVERSION this needs to be handled better if credentials is undefined or which union type password: credentials.password || credentials.token, }); -export const gitCallbacks = (credentials?: GitCredentials) => ({ +export const gitCallbacks = (credentials?: GitCredentials | null) => ({ onMessage, onAuthFailure, onAuthSuccess, - onAuth: onAuth(credentials), + onAuth: onAuth(credentials || undefined), }); + +// unfortunately, as of @types/node:v14.14.32 this type is not exported so we have to hackily grab it from here. +export type BufferEncoding = NonNullable[1]>; diff --git a/packages/insomnia-app/app/sync/lib/__tests__/deterministicStringify.test.js b/packages/insomnia-app/app/sync/lib/__tests__/deterministicStringify.test.ts similarity index 86% rename from packages/insomnia-app/app/sync/lib/__tests__/deterministicStringify.test.js rename to packages/insomnia-app/app/sync/lib/__tests__/deterministicStringify.test.ts index 2cced0085e..5a74a72c71 100644 --- a/packages/insomnia-app/app/sync/lib/__tests__/deterministicStringify.test.js +++ b/packages/insomnia-app/app/sync/lib/__tests__/deterministicStringify.test.ts @@ -7,25 +7,30 @@ describe('deterministicStringify()', () => { a: 'a', b: 'b', }); - expect(result).toBe('{"a":"a","b":"b","c":"c"}'); }); it('works recursively', () => { const result = deterministicStringify({ - arr: [{ b: 'b', a: 'a' }], + arr: [ + { + b: 'b', + a: 'a', + }, + ], obj: { - obj2: { b: 'b', a: 'a' }, + obj2: { + b: 'b', + a: 'a', + }, }, }); - expect(result).toBe('{"arr":[{"a":"a","b":"b"}],"obj":{"obj2":{"a":"a","b":"b"}}}'); }); it('works with strange types', () => { const sDate = deterministicStringify(new Date(1541178019555)); expect(sDate).toBe('"2018-11-02T17:00:19.555Z"'); - const sNull = deterministicStringify(null); expect(sNull).toBe('null'); }); diff --git a/packages/insomnia-app/app/sync/lib/deterministicStringify.js b/packages/insomnia-app/app/sync/lib/deterministicStringify.ts similarity index 83% rename from packages/insomnia-app/app/sync/lib/deterministicStringify.js rename to packages/insomnia-app/app/sync/lib/deterministicStringify.ts index 661d29ea3d..27f906b9a2 100644 --- a/packages/insomnia-app/app/sync/lib/deterministicStringify.js +++ b/packages/insomnia-app/app/sync/lib/deterministicStringify.ts @@ -1,30 +1,34 @@ -// @flow - -export function deterministicStringify(value: any): string { +export function deterministicStringify(value: any) { const t = Object.prototype.toString.call(value); + if (t === '[object Object]') { - const pairs = []; + const pairs: Array = []; + for (const key of Object.keys(value).sort()) { const k = deterministicStringify(key); const v = deterministicStringify(value[key]); + if (v !== '' && k !== '') { pairs.push(`${k}:${v}`); } } + return `{${pairs.join(',')}}`; } else if (t === '[object Array]') { - const items = []; + const items: Array = []; + for (const v of value) { const vStr = deterministicStringify(v); + if (vStr !== '') { items.push(vStr); } } + return `[${items.join(',')}]`; } const str = JSON.stringify(value); - // Only return valid stringifyable things return str === undefined ? '' : str; } diff --git a/packages/insomnia-app/app/sync/store/__tests__/index.test.js b/packages/insomnia-app/app/sync/store/__tests__/index.test.ts similarity index 69% rename from packages/insomnia-app/app/sync/store/__tests__/index.test.js rename to packages/insomnia-app/app/sync/store/__tests__/index.test.ts index 7660bf7645..dfcec7b42d 100644 --- a/packages/insomnia-app/app/sync/store/__tests__/index.test.js +++ b/packages/insomnia-app/app/sync/store/__tests__/index.test.ts @@ -6,19 +6,20 @@ describe('store', () => { describe(Driver.name, () => { it('supports CRUD operations', async () => { const s = new Store(new Driver()); - expect(await s.hasItem('404')).toBe(false); expect(await s.getItem('404')).toBe(null); - expect(await s.setItem('foo', 'bar')).toBe(undefined); expect(await s.getItem('foo')).toBe('bar'); - expect(await s.setItem('null', null)).toBe(undefined); expect(await s.getItem('null')).toBe(null); - - expect(await s.setItem('obj', { foo: 'bar' })).toBe(undefined); - expect(await s.getItem('obj')).toEqual({ foo: 'bar' }); - + expect( + await s.setItem('obj', { + foo: 'bar', + }), + ).toBe(undefined); + expect(await s.getItem('obj')).toEqual({ + foo: 'bar', + }); expect(await s.removeItem('foo')).toBe(undefined); expect(await s.hasItem('foo')).toBe(false); expect(await s.getItem('foo')).toBe(null); @@ -26,24 +27,25 @@ describe('store', () => { it('clears all values', async () => { const s = new Store(new Driver()); - await s.setItem('a', 'aaa'); await s.setItem('b', 'bbb'); await s.clear(); - expect(await s.hasItem('a')).toBe(false); expect(await s.hasItem('b')).toBe(false); }); it('stores buffers directly', async () => { const s = new Store(new Driver()); - await s.setItem('buff', Buffer.from('{"hi": "there"}', 'utf8')); - await s.setItem('json', { hi: 'there' }); - - expect(await s.getItem('buff')).toEqual({ hi: 'there' }); - expect(await s.getItem('json')).toEqual({ hi: 'there' }); - + await s.setItem('json', { + hi: 'there', + }); + expect(await s.getItem('buff')).toEqual({ + hi: 'there', + }); + expect(await s.getItem('json')).toEqual({ + hi: 'there', + }); expect((await s._driver.getItem('buff')).toString('utf8')).toEqual('{"hi": "there"}'); expect((await s._driver.getItem('json')).toString('utf8')).toEqual('{\n "hi": "there"\n}'); }); @@ -54,13 +56,16 @@ describe('store', () => { const s = new Store(new MemoryDriver(), [ { // Just some dumb hooks to test with - read: (ext, buff) => Buffer.from(buff.toString('utf8').replace('WORLD', 'World')), - write: (ext, buff) => Buffer.from(buff.toString('utf8').replace('World', 'WORLD')), + read: (_ext, buff) => Buffer.from(buff.toString('utf8').replace('WORLD', 'World')), + write: (_ext, buff) => Buffer.from(buff.toString('utf8').replace('World', 'WORLD')), }, ]); - - await s.setItem('foo', { Hello: 'World!' }); + await s.setItem('foo', { + Hello: 'World!', + }); expect((await s._driver.getItem('foo')).toString('utf8')).toBe('{\n "Hello": "WORLD!"\n}'); - expect(await s.getItem('foo')).toEqual({ Hello: 'World!' }); + expect(await s.getItem('foo')).toEqual({ + Hello: 'World!', + }); }); }); diff --git a/packages/insomnia-app/app/sync/store/drivers/base.js b/packages/insomnia-app/app/sync/store/drivers/base.ts similarity index 82% rename from packages/insomnia-app/app/sync/store/drivers/base.js rename to packages/insomnia-app/app/sync/store/drivers/base.ts index 92586768b7..9eb41adfe1 100644 --- a/packages/insomnia-app/app/sync/store/drivers/base.js +++ b/packages/insomnia-app/app/sync/store/drivers/base.ts @@ -1,17 +1,9 @@ -// @flow - export interface BaseDriver { - constructor(config: { [string]: any }): void; - + new (config: Record): void; hasItem(key: string): Promise; - setItem(key: string, value: Buffer): Promise; - getItem(key: string): Promise; - removeItem(key: string): Promise; - keys(prefix: string, recursive: boolean): Promise>; - clear(): Promise; } diff --git a/packages/insomnia-app/app/sync/store/drivers/file-system-driver.js b/packages/insomnia-app/app/sync/store/drivers/file-system-driver.ts similarity index 80% rename from packages/insomnia-app/app/sync/store/drivers/file-system-driver.js rename to packages/insomnia-app/app/sync/store/drivers/file-system-driver.ts index d5a79382e3..6b06541898 100644 --- a/packages/insomnia-app/app/sync/store/drivers/file-system-driver.js +++ b/packages/insomnia-app/app/sync/store/drivers/file-system-driver.ts @@ -1,9 +1,9 @@ -// @flow import fs from 'fs'; import path from 'path'; import mkdirp from 'mkdirp'; import type { BaseDriver } from './base'; +// @ts-expect-error -- TSCONVERSION export default class FileSystemDriver implements BaseDriver { _directory: string; @@ -12,9 +12,9 @@ export default class FileSystemDriver implements BaseDriver { console.log(`[FileSystemDriver] Initialized in "${this._directory}"`); } - async hasItem(key: string): Promise { - return new Promise((resolve, reject) => { - fs.stat(this._getKeyPath(key), (err, result) => { + async hasItem(key: string) { + return new Promise((resolve, reject) => { + fs.stat(this._getKeyPath(key), err => { if (err && err.code === 'ENOENT') { resolve(false); } else if (err) { @@ -26,32 +26,33 @@ export default class FileSystemDriver implements BaseDriver { }); } - setItem(key: string, value: Buffer): Promise { - return new Promise((resolve, reject) => { + setItem(key: string, value: Buffer) { + return new Promise((resolve, reject) => { const finalPath = this._getKeyPath(key); // Temp path contains randomness to avoid race-condition collisions. This // doesn't actually avoid race conditions but at least it won't fail. const tmpPath = `${finalPath}.${Math.random()}.tmp`; - // This method implements atomic writes by first writing to a temporary // file (non-atomic) then renaming the file to the final value (atomic) fs.writeFile(tmpPath, value, 'utf8', err => { if (err) { return reject(err); } + fs.rename(tmpPath, finalPath, err => { if (err) { return reject(err); } + resolve(); }); }); }); } - getItem(key: string): Promise { - return new Promise((resolve, reject) => { + getItem(key: string) { + return new Promise((resolve, reject) => { fs.readFile(this._getKeyPath(key), (err, data) => { if (err && err.code === 'ENOENT') { resolve(null); @@ -64,8 +65,8 @@ export default class FileSystemDriver implements BaseDriver { }); } - removeItem(key: string): Promise { - return new Promise((resolve, reject) => { + removeItem(key: string) { + return new Promise((resolve, reject) => { fs.unlink(this._getKeyPath(key), err => { if (err && err.code === 'ENOENT') { resolve(); @@ -78,8 +79,8 @@ export default class FileSystemDriver implements BaseDriver { }); } - clear(): Promise { - return new Promise((resolve, reject) => { + clear() { + return new Promise((resolve, reject) => { fs.readdir(this._directory, (err, names) => { if (err) { return reject(err); @@ -94,11 +95,12 @@ export default class FileSystemDriver implements BaseDriver { }); } - async keys(prefix: string, recursive: boolean): Promise> { + async keys(prefix: string, recursive: boolean) { const next = dir => { - return new Promise(async (resolve, reject) => { + return new Promise>(async (resolve, reject) => { let keys: Array = []; - let names = []; + let names: Array = []; + try { names = fs.readdirSync(dir); } catch (err) { @@ -115,6 +117,7 @@ export default class FileSystemDriver implements BaseDriver { const p = path.join(dir, name); const isDir = fs.statSync(p).isDirectory(); + if (isDir && recursive) { const more = await next(p); keys = [...keys, ...more]; @@ -130,8 +133,8 @@ export default class FileSystemDriver implements BaseDriver { }; const rawKeys = await next(this._getKeyPath(prefix)); + const keys: Array = []; - const keys = []; for (const rawKey of rawKeys) { keys.push(rawKey.substring(this._directory.length)); } @@ -139,12 +142,10 @@ export default class FileSystemDriver implements BaseDriver { return keys; } - _getKeyPath(key: string): string { + _getKeyPath(key: string) { const p = path.join(this._directory, key); - // Create base directory mkdirp.sync(path.dirname(p)); - return p; } } diff --git a/packages/insomnia-app/app/sync/store/drivers/memory-driver.js b/packages/insomnia-app/app/sync/store/drivers/memory-driver.ts similarity index 66% rename from packages/insomnia-app/app/sync/store/drivers/memory-driver.js rename to packages/insomnia-app/app/sync/store/drivers/memory-driver.ts index b083c6d631..817905ee8a 100644 --- a/packages/insomnia-app/app/sync/store/drivers/memory-driver.js +++ b/packages/insomnia-app/app/sync/store/drivers/memory-driver.ts @@ -1,24 +1,23 @@ -// @flow - import type { BaseDriver } from './base'; +// @ts-expect-error -- TSCONVERSION export default class MemoryDriver implements BaseDriver { - _db: { [string]: Buffer }; + _db: Record; constructor() { this._init(); } - async hasItem(key: string): Promise { + async hasItem(key: string) { return this._db[String(key)] instanceof Buffer; } - async setItem(key: string, value: Buffer): Promise { + async setItem(key: string, value: Buffer) { this._db[String(key)] = value; } - async getItem(key: string): Promise { - let value = null; + async getItem(key: string) { + let value: Buffer | null = null; if (await this.hasItem(key)) { value = this._db[key]; @@ -27,16 +26,16 @@ export default class MemoryDriver implements BaseDriver { return value; } - async removeItem(key: string): Promise { + async removeItem(key: string) { delete this._db[String(key)]; } - async clear(): Promise { + async clear() { this._init(); } - async keys(prefix: string, recursive: boolean): Promise> { - const keys = []; + async keys(prefix: string, recursive: boolean) { + const keys: Array = []; const baseLevels = prefix.split('/').length; for (const key of Object.keys(this._db)) { diff --git a/packages/insomnia-app/app/sync/store/hooks/__tests__/compress.test.js b/packages/insomnia-app/app/sync/store/hooks/__tests__/compress.test.ts similarity index 100% rename from packages/insomnia-app/app/sync/store/hooks/__tests__/compress.test.js rename to packages/insomnia-app/app/sync/store/hooks/__tests__/compress.test.ts diff --git a/packages/insomnia-app/app/sync/store/hooks/compress.js b/packages/insomnia-app/app/sync/store/hooks/compress.ts similarity index 90% rename from packages/insomnia-app/app/sync/store/hooks/compress.js rename to packages/insomnia-app/app/sync/store/hooks/compress.ts index 1d70d92c4e..8cde360fd4 100644 --- a/packages/insomnia-app/app/sync/store/hooks/compress.js +++ b/packages/insomnia-app/app/sync/store/hooks/compress.ts @@ -1,8 +1,7 @@ -// @flow import type { HookFn } from '../index'; import zlib from 'zlib'; -const read: HookFn = async function read(extension: string, value: Buffer): Promise { +const read: HookFn = async function read(extension: string, value: Buffer) { if (extension) { return value; } @@ -18,7 +17,7 @@ const read: HookFn = async function read(extension: string, value: Buffer): Prom }); }; -const write: HookFn = async function read(extension: string, value: Buffer): Promise { +const write: HookFn = async function read(extension: string, value: Buffer) { if (extension) { return value; } diff --git a/packages/insomnia-app/app/sync/store/index.js b/packages/insomnia-app/app/sync/store/index.ts similarity index 81% rename from packages/insomnia-app/app/sync/store/index.js rename to packages/insomnia-app/app/sync/store/index.ts index a7cecadb50..4339c015f8 100644 --- a/packages/insomnia-app/app/sync/store/index.js +++ b/packages/insomnia-app/app/sync/store/index.ts @@ -1,12 +1,15 @@ -// @flow - import path from 'path'; import type { BaseDriver } from './drivers/base'; // Can't really make this any more specific unfortunately type JSONValue = any; + export type HookFn = (extension: string, value: Buffer) => Promise; -export type Hook = { read: HookFn, write: HookFn }; + +export interface Hook { + read: HookFn; + write: HookFn; +} export default class Store { _driver: BaseDriver; @@ -17,13 +20,14 @@ export default class Store { this._hooks = hooks || []; } - async hasItem(key: string): Promise { + async hasItem(key: string) { return this._driver.hasItem(key); } - async setItem(key: string, value: JSONValue | Buffer): Promise { + async setItem(key: string, value: JSONValue | Buffer) { const ext = path.extname(key); let serializedValue; + try { serializedValue = await this._serialize(ext, value); } catch (err) { @@ -33,19 +37,20 @@ export default class Store { return this._driver.setItem(key, serializedValue); } - async setItemRaw(key: string, value: Buffer): Promise { + async setItemRaw(key: string, value: Buffer) { return this._driver.setItem(key, value); } async getItem(key: string): Promise { const rawValue = await this.getItemRaw(key); + if (rawValue === null) { return null; } const ext = path.extname(key); - let value; + try { // Without the `await` here, the catch won't get called value = await this._deserialize(ext, rawValue); @@ -61,19 +66,19 @@ export default class Store { return this._driver.getItem(key); } - async removeItem(key: string): Promise { + async removeItem(key: string) { return this._driver.removeItem(key); } - async keys(prefix: string, recursive: boolean = true): Promise> { + async keys(prefix: string, recursive = true) { return this._driver.keys(prefix, recursive); } - async clear(): Promise { + async clear() { return this._driver.clear(); } - async _serialize(ext: string, raw: JSONValue | Buffer): Promise { + async _serialize(ext: string, raw: JSONValue | Buffer) { let buff = raw instanceof Buffer ? raw : Buffer.from(JSON.stringify(raw, null, 2), 'utf8'); for (const hook of this._hooks) { diff --git a/packages/insomnia-app/app/sync/types.js b/packages/insomnia-app/app/sync/types.js deleted file mode 100644 index 6beb6f7c6f..0000000000 --- a/packages/insomnia-app/app/sync/types.js +++ /dev/null @@ -1,107 +0,0 @@ -// @flow - -export type Team = { - id: string, - name: string, -}; - -export type Project = { - id: string, - name: string, - rootDocumentId: string, -}; - -export type DocumentKey = string; -export type BlobId = string; - -export type Head = {| - branch: string, -|}; - -export type SnapshotStateEntry = {| - key: DocumentKey, - blob: BlobId, - name: string, -|}; - -export type SnapshotState = Array; -export type SnapshotStateMap = { [DocumentKey]: SnapshotStateEntry }; -export type SnapshotId = string; - -export type Snapshot = {| - id: SnapshotId, - created: Date, - parent: string, - author: string, - name: string, - description: string, - state: Array, - - // Only exists in Snapshots that are pulled from the server - authorAccount?: { - firstName: string, - lastName: string, - email: string, - }, -|}; - -export type Branch = {| - name: string, - created: Date, - modified: Date, - snapshots: Array, -|}; - -export type StageEntryDelete = {| - deleted: true, - key: string, - name: string, - blobId: string, -|}; - -export type StageEntryAdd = {| - added: true, - key: string, - name: string, - blobId: string, - blobContent: string, -|}; - -export type StageEntryModify = {| - modified: true, - key: string, - name: string, - blobId: string, - blobContent: string, -|}; - -export type StageEntry = StageEntryDelete | StageEntryAdd | StageEntryModify; - -export type MergeConflict = {| - name: string, - key: DocumentKey, - message: string, - mineBlob: BlobId | null, - theirsBlob: BlobId | null, - choose: BlobId | null, -|}; - -export type Stage = { - [DocumentKey]: StageEntry, -}; - -export type StatusCandidate = {| - key: DocumentKey, - name: string, - document: Object, -|}; - -export type StatusCandidateMap = { [DocumentKey]: StatusCandidate }; - -export type Status = {| - key: string, - stage: Stage, - unstaged: { - [DocumentKey]: StageEntry, - }, -|}; diff --git a/packages/insomnia-app/app/sync/types.ts b/packages/insomnia-app/app/sync/types.ts new file mode 100644 index 0000000000..ee7d58bbfd --- /dev/null +++ b/packages/insomnia-app/app/sync/types.ts @@ -0,0 +1,104 @@ + +export interface Team { + id: string; + name: string; +} + +export interface Project { + id: string; + name: string; + rootDocumentId: string; +} + +export type DocumentKey = string; + +export type BlobId = string; + +export interface Head { + branch: string; +} + +export interface SnapshotStateEntry { + key: DocumentKey; + blob: BlobId; + name: string; +} + +export type SnapshotState = Array; + +export type SnapshotStateMap = Record; + +export type SnapshotId = string; + +export interface Snapshot { + id: SnapshotId; + created: Date; + parent: string; + author: string; + name: string; + description: string; + state: Array; + // Only exists in Snapshots that are pulled from the server + authorAccount?: { + firstName: string; + lastName: string; + email: string; + }; +} + +export interface Branch { + name: string; + created: Date; + modified: Date; + snapshots: Array; +} + +export interface StageEntryDelete { + deleted: true; + key: string; + name: string; + blobId: string; +} + +export interface StageEntryAdd { + added: true; + key: string; + name: string; + blobId: string; + blobContent: string; +} + +export interface StageEntryModify { + modified: true; + key: string; + name: string; + blobId: string; + blobContent: string; +} + +export type StageEntry = StageEntryDelete | StageEntryAdd | StageEntryModify; + +export interface MergeConflict { + name: string; + key: DocumentKey; + message: string; + mineBlob: BlobId | null; + theirsBlob: BlobId | null; + choose: BlobId | null; +} + +export type Stage = Record; + +export interface StatusCandidate { + key: DocumentKey; + name: string; + document: Record; +} + +export type StatusCandidateMap = Record; + +export interface Status { + key: string; + stage: Stage; + unstaged: Record; +} diff --git a/packages/insomnia-app/app/sync/vcs/__tests__/index.test.js b/packages/insomnia-app/app/sync/vcs/__tests__/index.test.ts similarity index 77% rename from packages/insomnia-app/app/sync/vcs/__tests__/index.test.js rename to packages/insomnia-app/app/sync/vcs/__tests__/index.test.ts index 32eda93802..ac881e06b8 100644 --- a/packages/insomnia-app/app/sync/vcs/__tests__/index.test.js +++ b/packages/insomnia-app/app/sync/vcs/__tests__/index.test.ts @@ -4,7 +4,9 @@ import { describeChanges } from '../util'; import { globalBeforeEach } from '../../../__jest__/before-each'; function newDoc(id) { - return { id }; + return { + id, + }; } async function vcs(branch) { @@ -39,7 +41,6 @@ describe('VCS', () => { ], {}, ); - expect(status).toEqual({ key: '0cffac636df909fb4f8e9a25570e5012846b46fd', stage: {}, @@ -64,19 +65,28 @@ describe('VCS', () => { it('returns add/modify/delete operations', async () => { const v = await vcs('master'); - const status1 = await v.status( [ - { key: 'a', name: 'A', document: newDoc('aaa') }, - { key: 'b', name: 'B', document: newDoc('bbb') }, - { key: 'c', name: 'C', document: newDoc('ccc') }, + { + key: 'a', + name: 'A', + document: newDoc('aaa'), + }, + { + key: 'b', + name: 'B', + document: newDoc('bbb'), + }, + { + key: 'c', + name: 'C', + document: newDoc('ccc'), + }, ], {}, ); expect(Object.keys(status1.unstaged)).toEqual(['a', 'b', 'c']); - await v.stage(status1.stage, [status1.unstaged.a, status1.unstaged.b, status1.unstaged.c]); - await v.takeSnapshot(status1.stage, 'Add a/b/c'); const history = await v.getHistory(); expect(history.length).toBe(1); @@ -107,18 +117,32 @@ describe('VCS', () => { ], }, ]); - // Should get every operation type const status = await v.status( [ - { key: 'notA', name: 'Not A', document: newDoc('aaa') }, - { key: 'b', name: 'B', document: newDoc('bbb') }, - { key: 'c', name: 'C', document: newDoc('modified') }, - { key: 'd', name: 'D', document: newDoc('ddd') }, + { + key: 'notA', + name: 'Not A', + document: newDoc('aaa'), + }, + { + key: 'b', + name: 'B', + document: newDoc('bbb'), + }, + { + key: 'c', + name: 'C', + document: newDoc('modified'), + }, + { + key: 'd', + name: 'D', + document: newDoc('ddd'), + }, ], {}, ); - expect(status).toEqual({ key: '80834d815272c73192291477f70dce8a7c8bc424', stage: {}, @@ -152,24 +176,37 @@ describe('VCS', () => { }, }, }); - const newStage = await v.stage(status.stage, [ status.unstaged.a, status.unstaged.notA, status.unstaged.c, status.unstaged.d, ]); - const status2 = await v.status( [ - { key: 'notA', name: 'Not A', document: newDoc('aaa') }, - { key: 'b', name: 'B', document: newDoc('bbb') }, - { key: 'c', name: 'C', document: newDoc('modified') }, - { key: 'd', name: 'D', document: newDoc('ddd') }, + { + key: 'notA', + name: 'Not A', + document: newDoc('aaa'), + }, + { + key: 'b', + name: 'B', + document: newDoc('bbb'), + }, + { + key: 'c', + name: 'C', + document: newDoc('modified'), + }, + { + key: 'd', + name: 'D', + document: newDoc('ddd'), + }, ], newStage, ); - expect(status2).toEqual({ key: '4a0e8fd9ee30acb9c8ff4eb2f73e413ff7175a8c', stage: { @@ -207,24 +244,37 @@ describe('VCS', () => { it('can appear both staged and unstaged', async () => { const v = await vcs('master'); - const status = await v.status( [ - { key: 'a', name: 'A', document: newDoc('aaa') }, - { key: 'b', name: 'B', document: newDoc('bbb') }, + { + key: 'a', + name: 'A', + document: newDoc('aaa'), + }, + { + key: 'b', + name: 'B', + document: newDoc('bbb'), + }, ], {}, ); const stage = await v.stage(status.stage, [status.unstaged.a]); - const status2 = await v.status( [ - { key: 'a', name: 'A', document: newDoc('modified') }, - { key: 'b', name: 'B', document: newDoc('bbb') }, + { + key: 'a', + name: 'A', + document: newDoc('modified'), + }, + { + key: 'b', + name: 'B', + document: newDoc('bbb'), + }, ], stage, ); - expect(status2).toEqual({ key: '886337817e3c1ff7df3fcfa7c30192eb854b45cf', stage: { @@ -257,12 +307,28 @@ describe('VCS', () => { it('should not show committed entities', async () => { const v = await vcs('master'); - - const status = await v.status([{ key: 'foo', name: 'Foo', document: newDoc('bar') }], {}); + const status = await v.status( + [ + { + key: 'foo', + name: 'Foo', + document: newDoc('bar'), + }, + ], + {}, + ); const stage2 = await v.stage(status.stage, [status.unstaged.foo]); await v.takeSnapshot(stage2, 'Add foo'); - - const status2 = await v.status([{ key: 'foo', name: 'Foo', document: newDoc('bar') }], {}); + const status2 = await v.status( + [ + { + key: 'foo', + name: 'Foo', + document: newDoc('bar'), + }, + ], + {}, + ); expect(status2).toEqual({ key: 'ca455b43c1e992812d81032e79e037a8db85fa8b', stage: {}, @@ -274,15 +340,21 @@ describe('VCS', () => { describe('stage()', () => { it('stages entity', async () => { const v = await vcs('master'); - const status = await v.status( [ - { key: 'foo', name: 'Foo', document: newDoc('bar') }, - { key: 'baz', name: 'Baz', document: newDoc('qux') }, + { + key: 'foo', + name: 'Foo', + document: newDoc('bar'), + }, + { + key: 'baz', + name: 'Baz', + document: newDoc('qux'), + }, ], {}, ); - const stage = await v.stage(status.stage, [status.unstaged.foo]); expect(stage).toEqual({ foo: { @@ -293,11 +365,18 @@ describe('VCS', () => { added: true, }, }); - const status2 = await v.status( [ - { key: 'foo', name: 'Foo', document: newDoc('bar') }, - { key: 'baz', name: 'Baz', document: newDoc('qux') }, + { + key: 'foo', + name: 'Foo', + document: newDoc('bar'), + }, + { + key: 'baz', + name: 'Baz', + document: newDoc('qux'), + }, ], stage, ); @@ -328,11 +407,18 @@ describe('VCS', () => { describe('takeSnapshot()', () => { it('commits basic entity', async () => { const v = await vcs('master'); - - const status = await v.status([{ key: 'foo', name: 'Foo', document: newDoc('bar') }], {}); + const status = await v.status( + [ + { + key: 'foo', + name: 'Foo', + document: newDoc('bar'), + }, + ], + {}, + ); const stage = await v.stage(status.stage, [status.unstaged.foo]); await v.takeSnapshot(stage, 'Add foo'); - const history = await v.getHistory(); expect(history).toEqual([ { @@ -355,11 +441,18 @@ describe('VCS', () => { it('commits deleted entity', async () => { const v = await vcs('master'); - - const status = await v.status([{ key: 'foo', name: 'Foo', document: newDoc('bar') }], {}); + const status = await v.status( + [ + { + key: 'foo', + name: 'Foo', + document: newDoc('bar'), + }, + ], + {}, + ); const stage = await v.stage(status.stage, [status.unstaged.foo]); await v.takeSnapshot(stage, 'Add foo'); - const history = await v.getHistory(); expect(history).toEqual([ { @@ -378,12 +471,10 @@ describe('VCS', () => { ], }, ]); - const status2 = await v.status([], {}); const stage2 = await v.stage(status2.stage, [status2.unstaged.foo]); await v.takeSnapshot(stage2, 'Delete foo'); const history2 = await v.getHistory(); - expect(history2).toEqual([ { id: '365c4341f6d57e18994df2429387b78f2c9a31cb', @@ -416,10 +507,8 @@ describe('VCS', () => { describe('getBranches()', () => { it('lists branches', async () => { const v = await vcs('master'); - await v.checkout([], 'branch-1'); await v.checkout([], 'branch-2'); - const branches = await v.getBranches(); expect(branches).toEqual(['master', 'branch-1', 'branch-2']); }); @@ -428,8 +517,8 @@ describe('VCS', () => { describe('removeBranch()', () => { it('cannot remove empty branch', async () => { const v = await vcs('master'); - let didError = false; + try { await v.removeBranch(); } catch (err) { @@ -441,8 +530,8 @@ describe('VCS', () => { it('cannot remove current branch', async () => { const v = await vcs('master'); - let didError = false; + try { await v.removeBranch('master'); } catch (err) { @@ -454,16 +543,22 @@ describe('VCS', () => { it('remove branch', async () => { const v = await vcs('master'); - // Add something to master - const status1 = await v.status([{ key: 'foo', name: 'Foo', document: newDoc('bar') }], {}); + const status1 = await v.status( + [ + { + key: 'foo', + name: 'Foo', + document: newDoc('bar'), + }, + ], + {}, + ); const stage1 = await v.stage(status1.stage, [status1.unstaged.foo]); await v.takeSnapshot(stage1, 'Add foo'); - // Checkout branch await v.checkout([], 'new-branch'); expect(await v.getBranches()).toEqual(['master', 'new-branch']); - // Back to master and delete other branch await v.checkout([], 'master'); await v.removeBranch('new-branch'); @@ -474,12 +569,19 @@ describe('VCS', () => { describe('fork()', () => { it('forks to a new branch', async () => { const v = await vcs('master'); - // Add something to master - const status1 = await v.status([{ key: 'foo', name: 'Foo', document: newDoc('bar') }], {}); + const status1 = await v.status( + [ + { + key: 'foo', + name: 'Foo', + document: newDoc('bar'), + }, + ], + {}, + ); const stage1 = await v.stage(status1.stage, [status1.unstaged.foo]); await v.takeSnapshot(stage1, 'Add foo'); - // Checkout branch await v.fork('new-branch'); await v.checkout([], 'new-branch'); @@ -508,28 +610,50 @@ describe('VCS', () => { describe('merge()', () => { it('performs fast-forward merge', async () => { const v = await vcs('master'); - const status1 = await v.status( [ - { key: 'a', name: 'A', document: newDoc('aaa') }, - { key: 'b', name: 'B', document: newDoc('bbb') }, + { + key: 'a', + name: 'A', + document: newDoc('aaa'), + }, + { + key: 'b', + name: 'B', + document: newDoc('bbb'), + }, ], {}, ); const stage1 = await v.stage(status1.stage, [status1.unstaged.a, status1.unstaged.b]); await v.takeSnapshot(stage1, 'Add A and B'); expect((await v.getHistory())[0].state).toEqual([ - expect.objectContaining({ key: 'a' }), - expect.objectContaining({ key: 'b' }), + expect.objectContaining({ + key: 'a', + }), + expect.objectContaining({ + key: 'b', + }), ]); - await v.fork('feature-a'); await v.checkout([], 'feature-a'); const status2 = await v.status( [ - { key: 'a', name: 'A', document: newDoc('aaa') }, - { key: 'b', name: 'B', document: newDoc('bbbbbbb') }, - { key: 'c', name: 'C', document: newDoc('ccc') }, + { + key: 'a', + name: 'A', + document: newDoc('aaa'), + }, + { + key: 'b', + name: 'B', + document: newDoc('bbbbbbb'), + }, + { + key: 'c', + name: 'C', + document: newDoc('ccc'), + }, ], status1.stage, ); @@ -537,20 +661,33 @@ describe('VCS', () => { await v.takeSnapshot(stage2, 'Add C, modify B'); expect((await v.getHistory())[1].state).toEqual( expect.arrayContaining([ - expect.objectContaining({ key: 'a' }), - expect.objectContaining({ key: 'b' }), - expect.objectContaining({ key: 'c' }), + expect.objectContaining({ + key: 'a', + }), + expect.objectContaining({ + key: 'b', + }), + expect.objectContaining({ + key: 'c', + }), ]), ); }); it('merges even if no common root', async () => { const v = await vcs('master'); - const status1 = await v.status( [ - { key: 'a', name: 'A', document: newDoc('aaa') }, - { key: 'b', name: 'B', document: newDoc('bbb') }, + { + key: 'a', + name: 'A', + document: newDoc('aaa'), + }, + { + key: 'b', + name: 'B', + document: newDoc('bbb'), + }, ], {}, ); @@ -560,10 +697,18 @@ describe('VCS', () => { it('does something', async () => { const v = await vcs('master'); - // Add a file to master expect(await v.getBranch()).toBe('master'); - const status1 = await v.status([{ key: 'a', name: 'A', document: newDoc('aaa') }], {}); + const status1 = await v.status( + [ + { + key: 'a', + name: 'A', + document: newDoc('aaa'), + }, + ], + {}, + ); const stage = await v.stage(status1.stage, [status1.unstaged.a]); await v.takeSnapshot(stage, 'Add A'); expect(await v.getHistory()).toEqual([ @@ -583,13 +728,20 @@ describe('VCS', () => { ], }, ]); - // Checkout new branch and add file await v.fork('new-branch'); await v.checkout([], 'new-branch'); expect(await v.getBranch()).toBe('new-branch'); - - const status2 = await v.status([{ key: 'b', name: 'B', document: newDoc('bbb') }], {}); + const status2 = await v.status( + [ + { + key: 'b', + name: 'B', + document: newDoc('bbb'), + }, + ], + {}, + ); const stage2 = await v.stage(status2.stage, [status2.unstaged.b]); await v.takeSnapshot(stage2, 'Add B'); expect(await v.getHistory()).toEqual([ @@ -629,7 +781,6 @@ describe('VCS', () => { ], }, ]); - // Merge new branch back into master await v.checkout([], 'master'); expect(await v.getBranch()).toBe('master'); @@ -676,20 +827,48 @@ describe('VCS', () => { describe('describeChanges()', () => { it('works with same object structure', async () => { - const a = { foo: 'bar', nested: { baz: 10 } }; - const b = { foo: 'baz', nested: { baz: 11 } }; + const a = { + foo: 'bar', + nested: { + baz: 10, + }, + }; + const b = { + foo: 'baz', + nested: { + baz: 11, + }, + }; expect(describeChanges(a, b)).toEqual(['foo', 'nested']); }); it('ignores modified key', () => { - const a = { foo: 'bar', nested: { baz: 10 }, modified: 10 }; - const b = { foo: 'baz', nested: { baz: 11 }, modified: 12 }; + const a = { + foo: 'bar', + nested: { + baz: 10, + }, + modified: 10, + }; + const b = { + foo: 'baz', + nested: { + baz: 11, + }, + modified: 12, + }; expect(describeChanges(a, b)).toEqual(['foo', 'nested']); }); it('skips invalid values', () => { const a = null; - const b = { foo: 'baz', nested: { baz: 11 }, modified: 12 }; + const b = { + foo: 'baz', + nested: { + baz: 11, + }, + modified: 12, + }; expect(describeChanges(a, b)).toEqual([]); }); }); @@ -698,16 +877,26 @@ describe('VCS', () => { let v; beforeEach(async () => { v = await vcs('master'); - const status1 = await v.status( - [{ key: 'foo', name: 'Foo', document: newDoc('foobar1') }], + [ + { + key: 'foo', + name: 'Foo', + document: newDoc('foobar1'), + }, + ], {}, ); const stage1 = await v.stage(status1.stage, [status1.unstaged.foo]); await v.takeSnapshot(stage1, 'Add foo'); - const status2 = await v.status( - [{ key: 'bar', name: 'Bar', document: newDoc('foobar2') }], + [ + { + key: 'bar', + name: 'Bar', + document: newDoc('foobar2'), + }, + ], {}, ); const stage2 = await v.stage(status2.stage, [status2.unstaged.bar]); @@ -757,16 +946,12 @@ describe('VCS', () => { it('returns recent history', async () => { const [s1, s2, ...others] = await v.getHistory(); - // There should only be two items expect(others).toHaveLength(0); - // Get the latest item expect(await v.getHistory(1)).toStrictEqual([s2]); - // Get the last 2 items expect(await v.getHistory(2)).toStrictEqual([s1, s2]); - // Get the last 3 items (only 2 exist) expect(await v.getHistory(3)).toStrictEqual([s1, s2]); }); diff --git a/packages/insomnia-app/app/sync/vcs/__tests__/paths.test.js b/packages/insomnia-app/app/sync/vcs/__tests__/paths.test.ts similarity index 100% rename from packages/insomnia-app/app/sync/vcs/__tests__/paths.test.js rename to packages/insomnia-app/app/sync/vcs/__tests__/paths.test.ts diff --git a/packages/insomnia-app/app/sync/vcs/__tests__/util.test.js b/packages/insomnia-app/app/sync/vcs/__tests__/util.test.ts similarity index 82% rename from packages/insomnia-app/app/sync/vcs/__tests__/util.test.js rename to packages/insomnia-app/app/sync/vcs/__tests__/util.test.ts index 6a1bb2be44..3e7056f925 100644 --- a/packages/insomnia-app/app/sync/vcs/__tests__/util.test.js +++ b/packages/insomnia-app/app/sync/vcs/__tests__/util.test.ts @@ -19,7 +19,6 @@ describe('util', () => { newStateEntry('doc_1', 'blob_1'), newStateEntry('doc_2', 'blob_2'), ]); - const map = generateSnapshotStateMap(snapshot); expect(Object.keys(map).sort()).toEqual(['doc_1', 'doc_2']); }); @@ -31,7 +30,6 @@ describe('util', () => { newStateEntry('doc_2', 'blob_2'), newStateEntry('doc_2', 'blob_2'), ]); - const map = generateSnapshotStateMap(snapshot); expect(Object.keys(map).sort()).toEqual(['doc_1', 'doc_2']); }); @@ -49,7 +47,6 @@ describe('util', () => { newCandidate('doc_1', 1), newCandidate('doc_2', 2), ]; - const map = generateCandidateMap(candidates); expect(Object.keys(map).sort()).toEqual(['doc_1', 'doc_2']); }); @@ -64,19 +61,42 @@ describe('util', () => { newSnapshot(1, [newStateEntry('doc_4', 'blob_4'), newStateEntry('doc_1', 'blob_1')]), ); const map3 = generateSnapshotStateMap(newSnapshot(1, [])); - const keys = combinedMapKeys(map1, map2, map3); expect(keys.sort()).toEqual(['doc_1', 'doc_2', 'doc_4']); }); }); describe('threeWayMerge()', () => { - const A1 = { key: 'a', blob: 'a.1', name: '' }; - const A2 = { key: 'a', blob: 'a.2', name: '' }; - const A3 = { key: 'a', blob: 'a.3', name: '' }; - const B1 = { key: 'b', blob: 'b.1', name: '' }; - const B2 = { key: 'b', blob: 'b.2', name: '' }; - const C1 = { key: 'c', blob: 'c.1', name: '' }; + const A1 = { + key: 'a', + blob: 'a.1', + name: '', + }; + const A2 = { + key: 'a', + blob: 'a.2', + name: '', + }; + const A3 = { + key: 'a', + blob: 'a.3', + name: '', + }; + const B1 = { + key: 'b', + blob: 'b.1', + name: '', + }; + const B2 = { + key: 'b', + blob: 'b.2', + name: '', + }; + const C1 = { + key: 'c', + blob: 'c.1', + name: '', + }; it('does nothing with empty states', () => { const newState = threeWayMerge([], [], []); @@ -140,7 +160,6 @@ describe('util', () => { const root = [A1]; const trunk = [A2]; const other = [A3]; - expect(threeWayMerge(root, trunk, other)).toEqual({ conflicts: [ { @@ -160,7 +179,6 @@ describe('util', () => { const root = [A1]; const trunk = [A1, B1]; const other = [A1, B1]; - expect(threeWayMerge(root, trunk, other)).toEqual({ conflicts: [], state: [A1, B1], @@ -171,7 +189,6 @@ describe('util', () => { const root = [A1]; const trunk = [A1, B1]; const other = [A1, B2]; - expect(threeWayMerge(root, trunk, other)).toEqual({ conflicts: [ { @@ -191,7 +208,6 @@ describe('util', () => { const root = [A1, B1]; const trunk = [A1, B2]; const other = [A1]; - expect(threeWayMerge(root, trunk, other)).toEqual({ conflicts: [ { @@ -211,7 +227,6 @@ describe('util', () => { const root = [A1, B1]; const trunk = [A1]; const other = [A1, B2]; - expect(threeWayMerge(root, trunk, other)).toEqual({ conflicts: [ { @@ -231,7 +246,6 @@ describe('util', () => { const root = []; const trunk = [A1, C1]; const other = [A1, B2]; - expect(threeWayMerge(root, trunk, other)).toEqual({ conflicts: [], state: expect.arrayContaining([A1, B2, C1]), @@ -242,7 +256,6 @@ describe('util', () => { const root = []; const trunk = [A1]; const other = [A2]; - expect(threeWayMerge(root, trunk, other)).toEqual({ conflicts: [ { @@ -260,16 +273,35 @@ describe('util', () => { }); describe('stateDelta()', () => { - const A1 = { key: 'a', blob: 'a.1', name: '' }; - const A2 = { key: 'a', blob: 'a.2', name: '' }; - const B1 = { key: 'b', blob: 'b.1', name: '' }; - const B2 = { key: 'b', blob: 'b.2', name: '' }; - const C1 = { key: 'c', blob: 'c.1', name: '' }; + const A1 = { + key: 'a', + blob: 'a.1', + name: '', + }; + const A2 = { + key: 'a', + blob: 'a.2', + name: '', + }; + const B1 = { + key: 'b', + blob: 'b.1', + name: '', + }; + const B2 = { + key: 'b', + blob: 'b.2', + name: '', + }; + const C1 = { + key: 'c', + blob: 'c.1', + name: '', + }; it('modifies an entry', () => { const base = [A1]; const dsrd = [A2]; - expect(stateDelta(base, dsrd)).toEqual({ add: [], update: [A2], @@ -280,7 +312,6 @@ describe('util', () => { it('adds an entry', () => { const base = []; const dsrd = [B1]; - expect(stateDelta(base, dsrd)).toEqual({ add: [B1], update: [], @@ -291,7 +322,6 @@ describe('util', () => { it('deletes an entry', () => { const base = [A1]; const dsrd = []; - expect(stateDelta(base, dsrd)).toEqual({ add: [], update: [], @@ -302,7 +332,6 @@ describe('util', () => { it('complex combo', () => { const base = [A1, B1, C1]; const dsrd = [B2, C1]; - expect(stateDelta(base, dsrd)).toEqual({ add: [], update: [B2], @@ -313,7 +342,6 @@ describe('util', () => { it('invalid duplicate key state', () => { const base = [A1, A2]; const dsrd = [A2]; - expect(stateDelta(base, dsrd)).toEqual({ add: [], update: [], @@ -323,21 +351,75 @@ describe('util', () => { }); describe('getStagable()', () => { - const A1 = { key: 'a', blob: '1932cc749f57a77ba4a231d62559c47ee931ee2c', name: '' }; - const A2 = { key: 'a', blob: 'cf2ec18c2e686b4c966725f30050120d42d4b2e5', name: '' }; - const B1 = { key: 'b', blob: '05ab2ec03c7e32eff27305b4a11c573a3843fa01', name: '' }; - const B2 = { key: 'b', blob: 'eb157a78ebd367a004c5a32a18d6d8c18e60ce00', name: '' }; - const C1 = { key: 'c', blob: 'db7d35b96291b823e674bf23ac59e6e116b1890e', name: '' }; - const DA1 = { key: 'a', document: { id: 'a', v: 1 }, name: '' }; - const DA2 = { key: 'a', document: { id: 'a', v: 2 }, name: '' }; - const DB1 = { key: 'b', document: { id: 'b', v: 1 }, name: '' }; - const DB2 = { key: 'b', document: { id: 'b', v: 2 }, name: '' }; - const DC1 = { key: 'c', document: { id: 'c', v: 1 }, name: '' }; + const A1 = { + key: 'a', + blob: '1932cc749f57a77ba4a231d62559c47ee931ee2c', + name: '', + }; + const A2 = { + key: 'a', + blob: 'cf2ec18c2e686b4c966725f30050120d42d4b2e5', + name: '', + }; + const B1 = { + key: 'b', + blob: '05ab2ec03c7e32eff27305b4a11c573a3843fa01', + name: '', + }; + const B2 = { + key: 'b', + blob: 'eb157a78ebd367a004c5a32a18d6d8c18e60ce00', + name: '', + }; + const C1 = { + key: 'c', + blob: 'db7d35b96291b823e674bf23ac59e6e116b1890e', + name: '', + }; + const DA1 = { + key: 'a', + document: { + id: 'a', + v: 1, + }, + name: '', + }; + const DA2 = { + key: 'a', + document: { + id: 'a', + v: 2, + }, + name: '', + }; + const DB1 = { + key: 'b', + document: { + id: 'b', + v: 1, + }, + name: '', + }; + const DB2 = { + key: 'b', + document: { + id: 'b', + v: 2, + }, + name: '', + }; + const DC1 = { + key: 'c', + document: { + id: 'c', + v: 1, + }, + name: '', + }; it('works with empty state', () => { const state = []; const dsrd = [DA1]; - expect(getStagable(state, dsrd)).toEqual([ { blobContent: '{"id":"a","v":1}', @@ -352,14 +434,12 @@ describe('util', () => { it('stays the same', () => { const state = [A1, B1, C1]; const dsrd = [DA1, DB1, DC1]; - expect(getStagable(state, dsrd)).toEqual([]); }); it('adds a document', () => { const state = [A1]; const dsrd = [DA1, DB1]; - expect(getStagable(state, dsrd)).toEqual([ { key: 'b', @@ -374,7 +454,6 @@ describe('util', () => { it('modifies a document', () => { const state = [B1]; const dsrd = [DB2]; - expect(getStagable(state, dsrd)).toEqual([ { key: 'b', @@ -389,7 +468,6 @@ describe('util', () => { it('deletes a document', () => { const state = [B1]; const dsrd = []; - expect(getStagable(state, dsrd)).toEqual([ { key: 'b', @@ -403,7 +481,6 @@ describe('util', () => { it('does a lot of things', () => { const state = [A1, C1]; const dsrd = [DA2, DB1]; - expect(getStagable(state, dsrd)).toEqual([ { blobContent: '{"id":"a","v":2}', @@ -433,28 +510,24 @@ describe('util', () => { it('works with empty states', () => { const a = newBranch([]); const b = newBranch([]); - expect(getRootSnapshot(a, b)).toBeNull(); }); it('works with different states', () => { const a = newBranch(['s1', 's2']); const b = newBranch(['s3', 's5']); - expect(getRootSnapshot(a, b)).toBeNull(); }); it('works with same states', () => { const a = newBranch(['s1', 's2', 's3']); const b = newBranch(['s1', 's2', 's3']); - expect(getRootSnapshot(a, b)).toBe('s3'); }); it('works with subset', () => { const a = newBranch(['s1']); const b = newBranch(['s1', 's2', 's3']); - expect(getRootSnapshot(a, b)).toBe('s1'); expect(getRootSnapshot(b, a)).toBe('s1'); }); @@ -462,7 +535,6 @@ describe('util', () => { it('works with missing middle', () => { const a = newBranch(['s1', 's3', 's4']); const b = newBranch(['s1', 's2', 's3', 's6']); - expect(getRootSnapshot(a, b)).toBe('s3'); expect(getRootSnapshot(b, a)).toBe('s3'); }); @@ -470,20 +542,47 @@ describe('util', () => { it('works with missing middle', () => { const a = newBranch(['s1', 's3']); const b = newBranch(['s1', 's2', 's3']); - expect(getRootSnapshot(a, b)).toBe('s3'); expect(getRootSnapshot(b, a)).toBe('s3'); }); }); describe('updateStateWithConflictResolutions()', () => { - const A1 = { key: 'a', blob: '1932cc749f57a77ba4a231d62559c47ee931ee2c', name: '' }; - const A2 = { key: 'a', blob: 'cf2ec18c2e686b4c966725f30050120d42d4b2e5', name: '' }; - const B1 = { key: 'b', blob: '05ab2ec03c7e32eff27305b4a11c573a3843fa01', name: '' }; - const C1 = { key: 'c', blob: 'db7d35b96291b823e674bf23ac59e6e116b1890e', name: '' }; - const D1 = { key: 'd', blob: 'f23ac59e6e11db7d35b96291b823e674b6b1890e', name: '' }; - const E1 = { key: 'e', blob: '1db7d35b96291f23ac59e6e1b823e674b6b1890e', name: '' }; - const F1 = { key: 'f', blob: '6291b823e674b6b137172d35b9f23ac59e6e190e', name: '' }; + const A1 = { + key: 'a', + blob: '1932cc749f57a77ba4a231d62559c47ee931ee2c', + name: '', + }; + const A2 = { + key: 'a', + blob: 'cf2ec18c2e686b4c966725f30050120d42d4b2e5', + name: '', + }; + const B1 = { + key: 'b', + blob: '05ab2ec03c7e32eff27305b4a11c573a3843fa01', + name: '', + }; + const C1 = { + key: 'c', + blob: 'db7d35b96291b823e674bf23ac59e6e116b1890e', + name: '', + }; + const D1 = { + key: 'd', + blob: 'f23ac59e6e11db7d35b96291b823e674b6b1890e', + name: '', + }; + const E1 = { + key: 'e', + blob: '1db7d35b96291f23ac59e6e1b823e674b6b1890e', + name: '', + }; + const F1 = { + key: 'f', + blob: '6291b823e674b6b137172d35b9f23ac59e6e190e', + name: '', + }; it('does it', () => { const state = [A1, B1, E1, F1]; @@ -517,26 +616,68 @@ describe('util', () => { choose: null, }, ]; - expect(updateStateWithConflictResolutions(state, conflicts)).toEqual([A1, E1, F1, C1]); }); }); describe('preMergeCheck()', () => { - const A1 = { key: 'a', blob: '1932cc749f57a77ba4a231d62559c47ee931ee2c', name: '' }; - const A2 = { key: 'a', blob: 'cf2ec18c2e686b4c966725f30050120d42d4b2e5', name: '' }; - const B1 = { key: 'b', blob: '05ab2ec03c7e32eff27305b4a11c573a3843fa01', name: '' }; - const B2 = { key: 'b', blob: 'eb157a78ebd367a004c5a32a18d6d8c18e60ce00', name: '' }; - const DA1 = { key: 'a', document: { id: 'a', v: 1 }, name: '' }; - const DA2 = { key: 'a', document: { id: 'a', v: 2 }, name: '' }; - const DB1 = { key: 'b', document: { id: 'b', v: 1 }, name: '' }; - const DC1 = { key: 'c', document: { id: 'c', v: 1 }, name: '' }; + const A1 = { + key: 'a', + blob: '1932cc749f57a77ba4a231d62559c47ee931ee2c', + name: '', + }; + const A2 = { + key: 'a', + blob: 'cf2ec18c2e686b4c966725f30050120d42d4b2e5', + name: '', + }; + const B1 = { + key: 'b', + blob: '05ab2ec03c7e32eff27305b4a11c573a3843fa01', + name: '', + }; + const B2 = { + key: 'b', + blob: 'eb157a78ebd367a004c5a32a18d6d8c18e60ce00', + name: '', + }; + const DA1 = { + key: 'a', + document: { + id: 'a', + v: 1, + }, + name: '', + }; + const DA2 = { + key: 'a', + document: { + id: 'a', + v: 2, + }, + name: '', + }; + const DB1 = { + key: 'b', + document: { + id: 'b', + v: 1, + }, + name: '', + }; + const DC1 = { + key: 'c', + document: { + id: 'c', + v: 1, + }, + name: '', + }; it('no changes', () => { const trunk = [A1]; const other = [A1]; const cands = [DA1]; - expect(preMergeCheck(trunk, other, cands)).toEqual({ conflicts: [], dirty: [], @@ -547,7 +688,6 @@ describe('util', () => { const trunk = [A1, B1]; const other = [A2, B2]; const cands = [DA1, DB1]; - expect(preMergeCheck(trunk, other, cands)).toEqual({ conflicts: [], dirty: [], @@ -558,7 +698,6 @@ describe('util', () => { const trunk = [A1]; const other = [A1]; const cands = []; - expect(preMergeCheck(trunk, other, cands)).toEqual({ conflicts: [], dirty: [], @@ -569,7 +708,6 @@ describe('util', () => { const trunk = [A1]; const other = []; const cands = [DA2]; - expect(preMergeCheck(trunk, other, cands)).toEqual({ conflicts: [DA2], dirty: [], @@ -580,7 +718,6 @@ describe('util', () => { const trunk = []; const other = [A1]; const cands = [DA2]; - expect(preMergeCheck(trunk, other, cands)).toEqual({ conflicts: [DA2], dirty: [], @@ -591,7 +728,6 @@ describe('util', () => { const trunk = [A1]; const other = [A1]; const cands = [DA2]; - expect(preMergeCheck(trunk, other, cands)).toEqual({ conflicts: [], dirty: [DA2], @@ -602,7 +738,6 @@ describe('util', () => { const trunk = [A1]; const other = [A2]; const cands = [DA2]; - expect(preMergeCheck(trunk, other, cands)).toEqual({ conflicts: [], dirty: [], @@ -613,7 +748,6 @@ describe('util', () => { const trunk = [A1]; const other = [A2]; const cands = [DA1]; - expect(preMergeCheck(trunk, other, cands)).toEqual({ conflicts: [], dirty: [], @@ -624,7 +758,6 @@ describe('util', () => { const trunk = [A1]; const other = []; const cands = [DA2]; - expect(preMergeCheck(trunk, other, cands)).toEqual({ conflicts: [DA2], dirty: [], @@ -635,7 +768,6 @@ describe('util', () => { const trunk = [A1]; const other = [A2]; const cands = [DC1]; - expect(preMergeCheck(trunk, other, cands)).toEqual({ conflicts: [], dirty: [DC1], @@ -755,9 +887,16 @@ describe('util', () => { describe('hashDocument()', () => { it('ignore object key order', () => { - const result1 = hashDocument({ c: 'c', a: 'a', b: 'b' }); - const result2 = hashDocument({ a: 'a', b: 'b', c: 'c' }); - + const result1 = hashDocument({ + c: 'c', + a: 'a', + b: 'b', + }); + const result2 = hashDocument({ + a: 'a', + b: 'b', + c: 'c', + }); expect(result1.hash).toBe(result2.hash); expect(result1.hash).toBe('fed03259e8027e1fab2e2e57b402671bef7f3eb9'); expect(result2.hash).toBe('fed03259e8027e1fab2e2e57b402671bef7f3eb9'); @@ -767,19 +906,33 @@ describe('util', () => { it('works on recursive things', () => { const result1 = hashDocument({ - arr: [{ b: 'b', a: 'a' }], + arr: [ + { + b: 'b', + a: 'a', + }, + ], obj: { - obj2: { b: 'b', a: 'a' }, + obj2: { + b: 'b', + a: 'a', + }, }, }); - const result2 = hashDocument({ - arr: [{ b: 'b', a: 'a' }], + arr: [ + { + b: 'b', + a: 'a', + }, + ], obj: { - obj2: { a: 'a', b: 'b' }, + obj2: { + a: 'a', + b: 'b', + }, }, }); - expect(result1.hash).toBe(result2.hash); expect(result1.hash).toBe('7eaaa8a03bada54b403aeada681aad6892a28ab3'); expect(result2.hash).toBe('7eaaa8a03bada54b403aeada681aad6892a28ab3'); @@ -794,7 +947,6 @@ describe('util', () => { it('array order matters', () => { const result1 = hashDocument(['c', 'a', 'b']); const result2 = hashDocument(['a', 'b', 'c']); - expect(result1.hash).not.toBe(result2.hash); expect(result1.hash).toBe('edf94aad27c26bdcfe7477e0ed68991cbaedf8d8'); expect(result2.hash).toBe('e13460afb1e68af030bb9bee8344c274494661fa'); @@ -805,13 +957,11 @@ describe('util', () => { it('works with strange types', () => { const sDate1 = hashDocument(new Date(1541178019555)); const sDate2 = hashDocument('2018-11-02T17:00:19.555Z'); - expect(sDate1.hash).toBe(sDate2.hash); expect(sDate1.hash).toBe('ec0a18c47fb39de00204c57b5b492fd408394a01'); expect(sDate2.hash).toBe('ec0a18c47fb39de00204c57b5b492fd408394a01'); expect(sDate1.content.toString('utf8')).toBe('"2018-11-02T17:00:19.555Z"'); expect(sDate2.content.toString('utf8')).toBe('"2018-11-02T17:00:19.555Z"'); - const sNull2 = hashDocument('null'); expect(sNull2.hash).toBe('8c1030365643f1f4b7f00e3d88c0a3c555522b60'); expect(sNull2.content.toString('utf8')).toBe('"null"'); @@ -822,11 +972,9 @@ describe('util', () => { a: [0, () => null], fn: () => null, }); - const sFunc2 = hashDocument({ a: [0], }); - expect(sFunc1.hash).toBe(sFunc2.hash); expect(sFunc1.hash).toBe('38b742facb1034438d82cf1e294d9e71051bf120'); expect(sFunc2.hash).toBe('38b742facb1034438d82cf1e294d9e71051bf120'); @@ -841,14 +989,23 @@ describe('util', () => { } catch (err) { return; } + throw new Error('Expected hashDocument(undefined) to fail'); }); it('ignores object keys that do not matter', () => { - const result1 = hashDocument({ a: 'a', modified: 456 }); - const result2 = hashDocument({ a: 'a', modified: 123 }); - const result3 = hashDocument({ a: 'b', modified: 123 }); - + const result1 = hashDocument({ + a: 'a', + modified: 456, + }); + const result2 = hashDocument({ + a: 'a', + modified: 123, + }); + const result3 = hashDocument({ + a: 'b', + modified: 123, + }); expect(result1.hash).toBe(result2.hash); expect(result1.hash).not.toBe(result3.hash); }); diff --git a/packages/insomnia-app/app/sync/vcs/index.js b/packages/insomnia-app/app/sync/vcs/index.ts similarity index 84% rename from packages/insomnia-app/app/sync/vcs/index.js rename to packages/insomnia-app/app/sync/vcs/index.ts index 5865ee2a3e..b4eb499aaa 100644 --- a/packages/insomnia-app/app/sync/vcs/index.js +++ b/packages/insomnia-app/app/sync/vcs/index.ts @@ -1,5 +1,3 @@ -// @flow - import type { BaseDriver } from '../store/drivers/base'; import path from 'path'; import clone from 'clone'; @@ -17,7 +15,6 @@ import type { SnapshotState, Stage, StageEntry, - Status, StatusCandidate, Team, } from '../types'; @@ -38,10 +35,7 @@ import * as session from '../../account/session'; import * as fetch from '../../account/fetch'; import { strings } from '../../common/strings'; -const EMPTY_HASH = crypto - .createHash('sha1') - .digest('hex') - .replace(/./g, '0'); +const EMPTY_HASH = crypto.createHash('sha1').digest('hex').replace(/./g, '0'); type ConflictHandler = (conflicts: Array) => Promise>; @@ -49,52 +43,53 @@ export default class VCS { _store: Store; _driver: BaseDriver; _project: Project | null; - _conflictHandler: ?ConflictHandler; + _conflictHandler?: ConflictHandler | null; constructor(driver: BaseDriver, conflictHandler?: ConflictHandler) { this._store = new Store(driver, [compress]); this._conflictHandler = conflictHandler; this._driver = driver; - // To be set later this._project = null; } newInstance(): VCS { - const newVCS: VCS = (Object.assign({}, this): any); + const newVCS: VCS = Object.assign({}, this) as any; Object.setPrototypeOf(newVCS, VCS.prototype); return newVCS; } - async setProject(project: Project): Promise { + async setProject(project: Project) { this._project = project; console.log(`[sync] Activated project ${project.id}`); - // Store it because it might not be yet await this._storeProject(project); } - hasProject(): boolean { + hasProject() { return this._project !== null; } - async removeProjectsForRoot(rootDocumentId: string): Promise { + async removeProjectsForRoot(rootDocumentId: string) { const all = await this._allProjects(); const toRemove = all.filter(p => p.rootDocumentId === rootDocumentId); + for (const project of toRemove) { await this._removeProject(project); } } - async archiveProject(): Promise { + async archiveProject() { const projectId = this._projectId(); + await this._queryProjectArchive(projectId); await this._store.removeItem(paths.project(projectId)); this._project = null; } - async switchProject(rootDocumentId: string): Promise { + async switchProject(rootDocumentId: string) { const project = await this._getProjectByRootDocument(rootDocumentId); + if (project !== null) { await this.setProject(project); } else { @@ -102,35 +97,37 @@ export default class VCS { } } - async switchAndCreateProjectIfNotExist(rootDocumentId: string, name: string): Promise { + async switchAndCreateProjectIfNotExist(rootDocumentId: string, name: string) { const project = await this._getOrCreateProjectByRootDocument(rootDocumentId, name); await this.setProject(project); } - async teams(): Promise> { + async teams() { return this._queryTeams(); } - async projectTeams(): Promise> { + async projectTeams() { return this._queryProjectTeams(); } - async localProjects(): Promise> { + async localProjects() { return this._allProjects(); } - async remoteProjects(): Promise> { + async remoteProjects() { return this._queryProjects(); } - async blobFromLastSnapshot(key: string): Promise { + async blobFromLastSnapshot(key: string) { const branch = await this._getCurrentBranch(); const snapshot = await this._getLatestSnapshot(branch.name); + if (!snapshot) { return null; } const entry = snapshot.state.find(e => e.key === key); + if (!entry) { return null; } @@ -138,13 +135,13 @@ export default class VCS { return this._getBlob(entry.blob); } - async status(candidates: Array, baseStage: Stage): Promise { + async status(candidates: Array, baseStage: Stage) { const stage = clone(baseStage); const branch = await this._getCurrentBranch(); const snapshot: Snapshot | null = await this._getLatestSnapshot(branch.name); const state = snapshot ? snapshot.state : []; + const unstaged: Record = {}; - const unstaged: { [DocumentKey]: StageEntry } = {}; for (const entry of getStagable(state, candidates)) { const { key } = entry; const stageEntry = stage[key]; @@ -157,28 +154,33 @@ export default class VCS { return { stage, unstaged, - key: hashDocument({ stage, unstaged }).hash, + key: hashDocument({ + stage, + unstaged, + }).hash, }; } - async stage(stage: Stage, stageEntries: Array): Promise { - const blobsToStore: { [string]: string } = {}; + async stage(stage: Stage, stageEntries: Array) { + const blobsToStore: Record = {}; + for (const entry of stageEntries) { stage[entry.key] = entry; // Only store blobs if we're not deleting it + // @ts-expect-error -- TSCONVERSION type narrowing if (entry.added || entry.modified) { + // @ts-expect-error -- TSCONVERSION type narrowing blobsToStore[entry.blobId] = entry.blobContent; } } await this._storeBlobs(blobsToStore); console.log(`[sync] Staged ${stageEntries.map(e => e.name).join(', ')}`); - return stage; } - async unstage(stage: Stage, stageEntries: Array): Promise { + async unstage(stage: Stage, stageEntries: Array) { for (const entry of stageEntries) { delete stage[entry.key]; } @@ -187,7 +189,7 @@ export default class VCS { return stage; } - static validateBranchName(branchName: string): string { + static validateBranchName(branchName: string) { if (!branchName.match(/^[a-zA-Z0-9][a-zA-Z0-9-_.]{2,}$/)) { return ( 'Branch names must be at least 3 characters long and can only contain ' + @@ -198,15 +200,16 @@ export default class VCS { return ''; } - async compareRemoteBranch(): Promise<{ ahead: number, behind: number }> { + async compareRemoteBranch() { const localBranch = await this._getCurrentBranch(); const remoteBranch = await this._queryBranch(localBranch.name); return compareBranches(localBranch, remoteBranch); } - async fork(newBranchName: string): Promise { + async fork(newBranchName: string) { const branch = await this._getCurrentBranch(); const errMsg = VCS.validateBranchName(newBranchName); + if (errMsg) { throw new Error(errMsg); } @@ -221,13 +224,11 @@ export default class VCS { modified: new Date(), snapshots: (await this._getCurrentBranch()).snapshots, }; - await this._storeBranch(newBranch); - console.log(`[sync] Forked ${branch.name} to ${newBranchName}`); } - async removeRemoteBranch(branchName: string): Promise { + async removeRemoteBranch(branchName: string) { if (branchName === 'master') { throw new Error('Cannot delete master branch'); } @@ -236,7 +237,7 @@ export default class VCS { console.log(`[sync] Deleted remote branch ${branchName}`); } - async removeBranch(branchName: string): Promise { + async removeBranch(branchName: string) { const branchToDelete = await this._assertBranch(branchName); const currentBranch = await this._getCurrentBranch(); @@ -252,41 +253,33 @@ export default class VCS { console.log(`[sync] Deleted local branch ${branchName}`); } - async checkout( - candidates: Array, - branchName: string, - ): Promise<{ - upsert: Array, - remove: Array, - }> { + async checkout(candidates: Array, branchName: string) { const branchCurrent = await this._getCurrentBranch(); const latestSnapshotCurrent: Snapshot | null = await this._getLatestSnapshot( branchCurrent.name, ); const latestStateCurrent = latestSnapshotCurrent ? latestSnapshotCurrent.state : []; - const branchNext = await this._getOrCreateBranch(branchName); const latestSnapshotNext: Snapshot | null = await this._getLatestSnapshot(branchNext.name); const latestStateNext = latestSnapshotNext ? latestSnapshotNext.state : []; - // Perform pre-checkout checks const { conflicts, dirty } = preMergeCheck(latestStateCurrent, latestStateNext, candidates); + if (conflicts.length) { throw new Error('Please snapshot current changes before switching branches'); } - await this._storeHead({ branch: branchNext.name }); - + await this._storeHead({ + branch: branchNext.name, + }); const dirtyMap = generateCandidateMap(dirty); const delta = stateDelta(latestStateCurrent, latestStateNext); - // Filter out things that should stay dirty const add = delta.add.filter(e => !dirtyMap[e.key]); const update = delta.update.filter(e => !dirtyMap[e.key]); const remove = delta.remove.filter(e => !dirtyMap[e.key]); const upsert = [...add, ...update]; console.log(`[sync] Switched to branch ${branchName}`); - // Remove all dirty items from the delta so we keep them around return { upsert: await this._getBlobs(upsert.map(e => e.blob)), @@ -309,9 +302,10 @@ export default class VCS { return this._conflictHandler(conflicts); } - async allDocuments(): Promise { + async allDocuments(): Promise> { const branch = await this._getCurrentBranch(); const snapshot: Snapshot | null = await this._getLatestSnapshot(branch.name); + if (!snapshot) { throw new Error('Failed to get latest snapshot for all documents'); } @@ -319,14 +313,10 @@ export default class VCS { return this._getBlobs(snapshot.state.map(s => s.blob)); } - async rollbackToLatest( - candidates: Array, - ): Promise<{ - upsert: Array, - remove: Array, - }> { + async rollbackToLatest(candidates: Array) { const branch = await this._getCurrentBranch(); const latestSnapshot = await this._getLatestSnapshot(branch.name); + if (!latestSnapshot) { throw new Error('No snapshots to rollback to'); } @@ -334,40 +324,36 @@ export default class VCS { return this.rollback(latestSnapshot.id, candidates); } - async rollback( - snapshotId: string, - candidates: Array, - ): Promise<{ - upsert: Array, - remove: Array, - }> { + async rollback(snapshotId: string, candidates: Array) { const rollbackSnapshot: Snapshot | null = await this._getSnapshot(snapshotId); + if (rollbackSnapshot === null) { throw new Error(`Failed to find snapshot by id ${snapshotId}`); } - const potentialNewState: SnapshotState = candidates.map(c => ({ - key: c.key, - blob: hashDocument(c.document).hash, - name: c.name, + const potentialNewState: SnapshotState = candidates.map(candidate => ({ + key: candidate.key, + blob: hashDocument(candidate.document).hash, + name: candidate.name, })); const delta = stateDelta(potentialNewState, rollbackSnapshot.state); + // We need to treat removals of candidates differently because they may not yet have been stored as blobs. + const remove: Array = []; - // We need to treat removals of candidates differently because they may not - // yet have been stored as blobs. - const remove = []; - for (const e of delta.remove) { - const c = candidates.find(c => c.key === e.key); - if (!c) { + for (const entry of delta.remove) { + const candidate = candidates.find(candidate => candidate.key === entry.key); + + if (!candidate) { // Should never happen throw new Error('Failed to find removal in candidates'); } - remove.push(c.document); + + // @ts-expect-error -- TSCONVERSION not sure what this is actually supposed to be + remove.push(candidate.document); } console.log(`[sync] Rolled back to ${snapshotId}`); - const upsert = [...delta.update, ...delta.add]; return { upsert: await this._getBlobs(upsert.map(e => e.blob)), @@ -375,20 +361,20 @@ export default class VCS { }; } - async getHistoryCount(branchName?: string): Promise { + async getHistoryCount(branchName?: string) { const branch = branchName ? await this._getBranch(branchName) : await this._getCurrentBranch(); - return branch.snapshots.length; + return branch?.snapshots.length; } - async getHistory(count: number = 0): Promise> { + async getHistory(count = 0) { const branch = await this._getCurrentBranch(); - const snapshots = []; - + const snapshots: Array = []; const total = branch.snapshots.length; const slice = count <= 0 || count > total ? 0 : total - count; for (const id of branch.snapshots.slice(slice)) { const snapshot = await this._getSnapshot(id); + if (snapshot === null) { throw new Error(`Failed to get snapshot id=${id}`); } @@ -399,7 +385,7 @@ export default class VCS { return snapshots; } - async getBranch(): Promise { + async getBranch() { const branch = await this._getCurrentBranch(); return branch.name; } @@ -418,16 +404,13 @@ export default class VCS { candidates: Array, otherBranchName: string, snapshotMessage?: string, - ): Promise<{ - upsert: Array, - remove: Array, - }> { + ) { const branch = await this._getCurrentBranch(); console.log(`[sync] Merged branch ${otherBranchName} into ${branch.name}`); return this._merge(candidates, branch.name, otherBranchName, snapshotMessage); } - async takeSnapshot(stage: Stage, name: string): Promise { + async takeSnapshot(stage: Stage, name: string) { const branch: Branch = await this._getCurrentBranch(); const parent: Snapshot | null = await this._getLatestSnapshot(branch.name); @@ -455,29 +438,28 @@ export default class VCS { // Add the rest of the staged items for (const key of Object.keys(stage)) { const entry = stage[key]; + + // @ts-expect-error -- TSCONVERSION find out where this is coming from from the Stage union if (entry.deleted) { continue; } const { name, blobId: blob } = entry; - newState.push({ key, name, blob }); + newState.push({ + key, + name, + blob, + }); } const snapshot = await this._createSnapshotFromState(branch, newState, name); console.log(`[sync] Created snapshot ${snapshot.id} (${name})`); } - async pull( - candidates: Array, - ): Promise<{ - upsert: Array, - remove: Array, - }> { + async pull(candidates: Array) { await this._getOrCreateRemoteProject(); - const localBranch = await this._getCurrentBranch(); const tmpBranchForRemote = await this._fetch(localBranch.name + '.hidden', localBranch.name); - // Merge branch and ensure that we use the remote's history when merging const message = `Synced latest changes from ${localBranch.name}`; const delta = await this._merge( @@ -487,37 +469,41 @@ export default class VCS { message, true, ); - // Remove tmp branch await this._removeBranch(tmpBranchForRemote); console.log(`[sync] Pulled branch ${localBranch.name}`); - return delta; } - async shareWithTeam(teamId: string): Promise { + async shareWithTeam(teamId: string) { const { memberKeys, projectKey } = await this._queryProjectShareInstructions(teamId); - const { privateKey } = this._assertSession(); - const symmetricKey = crypt.decryptRSAWithJWK(privateKey, projectKey.encSymmetricKey); - const keys = []; + const { privateKey } = this._assertSession(); + + const symmetricKey = crypt.decryptRSAWithJWK(privateKey, projectKey.encSymmetricKey); + const keys: Array = []; + for (const { accountId, publicKey } of memberKeys) { const encSymmetricKey = crypt.encryptRSAWithJWK(JSON.parse(publicKey), symmetricKey); - keys.push({ accountId, encSymmetricKey }); + keys.push({ + accountId, + encSymmetricKey, + }); } await this._queryProjectShare(teamId, keys); console.log(`[sync] Shared project ${this._projectId()} with ${teamId}`); } - async unShareWithTeam(): Promise { + async unShareWithTeam() { await this._queryProjectUnShare(); console.log(`[sync] Unshared project ${this._projectId()}`); } - async _getOrCreateRemoteProject(): Promise { + async _getOrCreateRemoteProject() { const localProject = await this._assertProject(); let project = await this._queryProject(); + if (!project) { project = await this._queryCreateProject(localProject.rootDocumentId, localProject.name); } @@ -526,14 +512,14 @@ export default class VCS { return project; } - async push(): Promise { + async push() { await this._getOrCreateRemoteProject(); const branch = await this._getCurrentBranch(); - // Check branch history to make sure there are no conflicts let lastMatchingIndex = 0; const remoteBranch: Branch | null = await this._queryBranch(branch.name); const remoteBranchSnapshots = remoteBranch ? remoteBranch.snapshots : []; + for (; lastMatchingIndex < remoteBranchSnapshots.length; lastMatchingIndex++) { if (remoteBranchSnapshots[lastMatchingIndex] !== branch.snapshots[lastMatchingIndex]) { throw new Error('Remote history conflict. Please pull latest changes and try again'); @@ -542,13 +528,15 @@ export default class VCS { // Get the remaining snapshots to push const snapshotIdsToPush = branch.snapshots.slice(lastMatchingIndex); + if (snapshotIdsToPush.length === 0) { throw new Error('Already up to date'); } // Gather a list of snapshot state entries to push - const allBlobIds = new Set(); - const snapshots = []; + const allBlobIds = new Set(); + const snapshots: Array = []; + for (const id of snapshotIdsToPush) { const snapshot = await this._assertSnapshot(id); snapshots.push(snapshot); @@ -564,27 +552,32 @@ export default class VCS { await this._queryPushSnapshots(snapshots); } - async _fetch(localBranchName: string, remoteBranchName: string): Promise { + async _fetch(localBranchName: string, remoteBranchName: string) { const remoteBranch: Branch | null = await this._queryBranch(remoteBranchName); + if (!remoteBranch) { throw new Error(`The remote branch "${remoteBranchName}" does not exist`); } // Fetch snapshots and blobs from remote branch const snapshotsToFetch: Array = []; + for (const snapshotId of remoteBranch.snapshots) { const localSnapshot = await this._getSnapshot(snapshotId); + if (!localSnapshot) { snapshotsToFetch.push(snapshotId); } } // Find blobs to fetch - const blobsToFetch = new Set(); + const blobsToFetch = new Set(); const snapshots = await this._querySnapshots(snapshotsToFetch); + for (const snapshot of snapshots) { for (const { blob } of snapshot.state) { const hasBlob = await this._hasBlob(blob); + if (hasBlob) { continue; } @@ -597,16 +590,13 @@ export default class VCS { const ids = Array.from(blobsToFetch); const blobs = await this._queryBlobs(ids); await this._storeBlobsBuffer(blobs); - // Store the snapshots await this._storeSnapshots(snapshots); - // Create the new branch and save it const branch = clone(remoteBranch); branch.created = branch.modified = new Date(); branch.name = localBranchName; await this._storeBranch(branch); - return branch; } @@ -616,20 +606,15 @@ export default class VCS { otherBranchName: string, snapshotMessage?: string, useOtherBranchHistory?: boolean, - ): Promise<{ - upsert: Array, - remove: Array, - }> { + ) { const branchOther = await this._assertBranch(otherBranchName); const latestSnapshotOther: Snapshot | null = await this._getLatestSnapshot(branchOther.name); - const branchTrunk = await this._assertBranch(trunkBranchName); const rootSnapshotId = getRootSnapshot(branchTrunk, branchOther); const rootSnapshot: Snapshot | null = await this._getSnapshot(rootSnapshotId || 'n/a'); const latestSnapshotTrunk: Snapshot | null = await this._getLatestSnapshot(branchTrunk.name); const latestStateTrunk = latestSnapshotTrunk ? latestSnapshotTrunk.state : []; const latestStateOther = latestSnapshotOther ? latestSnapshotOther.state : []; - // Perform pre-merge checks const { conflicts: preConflicts, dirty } = preMergeCheck( latestStateTrunk, @@ -656,14 +641,12 @@ export default class VCS { await this._storeBranch(branchTrunk); } else { const rootState = rootSnapshot ? rootSnapshot.state : []; - console.log('[sync] Performing 3-way merge'); const { state: stateBeforeConflicts, conflicts: mergeConflicts } = threeWayMerge( rootState, latestStateTrunk, latestStateOther, ); - // Update state with conflict resolutions applied const conflictResolutions = await this.handleAnyConflicts(mergeConflicts, ''); const state = updateStateWithConflictResolutions(stateBeforeConflicts, conflictResolutions); @@ -679,10 +662,8 @@ export default class VCS { const newLatestSnapshot = await this._getLatestSnapshot(branchTrunk.name); const newLatestSnapshotState = newLatestSnapshot ? newLatestSnapshot.state : []; - const { add, update, remove } = stateDelta(latestStateTrunk, newLatestSnapshotState); const upsert = [...add, ...update]; - // Remove all dirty items from the delta so we keep them around const dirtyMap = generateCandidateMap(dirty); return { @@ -695,37 +676,48 @@ export default class VCS { branch: Branch, state: SnapshotState, name: string, - ): Promise { + ) { const parentId = branch.snapshots.length ? branch.snapshots[branch.snapshots.length - 1] : EMPTY_HASH; // Create the snapshot const id = _generateSnapshotID(parentId, this._projectId(), state); + const snapshot: Snapshot = { id, name, state, - author: '', // Will be set when pushed + author: '', + // Will be set when pushed parent: parentId, created: new Date(), description: '', }; - // Update the branch history branch.modified = new Date(); branch.snapshots.push(snapshot.id); - await this._storeBranch(branch); await this._storeSnapshot(snapshot); console.log(`[sync] Created snapshot '${name}' on ${branch.name}`); - return snapshot; } - async _runGraphQL(query: string, variables: { [string]: any }, name: string): Promise { + async _runGraphQL( + query: string, + variables: Record, + name: string, + ): Promise> { const { sessionId } = this._assertSession(); - const { data, errors } = await fetch.post('/graphql?' + name, { query, variables }, sessionId); + + const { data, errors } = await fetch.post( + '/graphql?' + name, + { + query, + variables, + }, + sessionId, + ); if (errors && errors.length) { console.log(`[sync] Failed to query ${name}`, errors); @@ -750,7 +742,6 @@ export default class VCS { }, 'missingBlobs', ); - return blobsMissing.missing; } @@ -770,12 +761,11 @@ export default class VCS { }, 'branches', ); - // TODO: Fix server returning null instead of empty list return branches || []; } - async _queryRemoveBranch(branchName: string): Promise { + async _queryRemoveBranch(branchName: string) { await this._runGraphQL( ` mutation ($projectId: ID!, $branch: String!) { @@ -806,12 +796,12 @@ export default class VCS { }, 'branch', ); - return branch; } - async _querySnapshots(allIds: Array): Promise> { - let allSnapshots = []; + async _querySnapshots(allIds: Array) { + let allSnapshots: Array = []; + for (const ids of chunkArray(allIds, 20)) { const { snapshots } = await this._runGraphQL( ` @@ -847,7 +837,7 @@ export default class VCS { return allSnapshots; } - async _queryPushSnapshots(allSnapshots: Array): Promise { + async _queryPushSnapshots(allSnapshots: Array) { const { accountId } = this._assertSession(); for (const snapshots of chunkArray(allSnapshots, 20)) { @@ -898,17 +888,15 @@ export default class VCS { }, 'snapshotsPush', ); - // Store them in case something has changed await this._storeSnapshots(snapshotsCreate); - console.log('[sync] Pushed snapshots', snapshotsCreate.map(s => s.id).join(', ')); } } - async _queryBlobs(allIds: Array): Promise<{ [string]: Buffer }> { + async _queryBlobs(allIds: Array) { const symmetricKey = await this._getProjectSymmetricKey(); - const result = {}; + const result: Record = {}; for (const ids of chunkArray(allIds, 50)) { const { blobs } = await this._runGraphQL( @@ -935,15 +923,19 @@ export default class VCS { return result; } - async _queryPushBlobs(allIds: Array): Promise { + async _queryPushBlobs(allIds: Array) { const symmetricKey = await this._getProjectSymmetricKey(); - const next = async (items: Array<{ id: string, content: string }>) => { + const next = async ( + items: Array<{ + id: string; + content: string; + }>, + ) => { const encodedBlobs = items.map(i => ({ id: i.id, content: i.content, })); - const { blobsCreate } = await this._runGraphQL( ` mutation ($projectId: ID!, $blobs: [BlobInput!]!) { @@ -958,28 +950,33 @@ export default class VCS { }, 'blobsCreate', ); - return blobsCreate.count; }; // Push each missing blob in batches of 2MB max let count = 0; - let batch = []; + let batch: Array<{ id: string; content: string }> = []; let batchSizeBytes = 0; const maxBatchSize = 1024 * 1024 * 2; // 2 MB + const maxBatchCount = 200; + for (let i = 0; i < allIds.length; i++) { const id = allIds[i]; const content = await this._getBlobRaw(id); + if (content === null) { throw new Error(`Failed to get blob id=${id}`); } const encryptedResult = crypt.encryptAESBuffer(symmetricKey, content); - batch.push({ id, content: JSON.stringify(encryptedResult, null, 2) }); - + batch.push({ + id, + content: JSON.stringify(encryptedResult, null, 2), + }); batchSizeBytes += content.length; const isLastId = i === allIds.length - 1; + if (batchSizeBytes > maxBatchSize || isLastId || batch.length >= maxBatchCount) { count += await next(batch); const batchSizeMB = Math.round((batchSizeBytes / 1024) * 100) / 100; @@ -992,7 +989,7 @@ export default class VCS { console.log(`[sync] Finished uploading ${count}/${allIds.length} blobs`); } - async _queryProjectKey(): Promise { + async _queryProjectKey() { const { projectKey } = await this._runGraphQL( ` query ($projectId: ID!) { @@ -1006,11 +1003,10 @@ export default class VCS { }, 'projectKey', ); - - return projectKey.encSymmetricKey; + return projectKey.encSymmetricKey as string; } - async _queryTeams(): Promise> { + async _queryTeams() { const { teams } = await this._runGraphQL( ` query { @@ -1023,11 +1019,10 @@ export default class VCS { {}, 'teams', ); - - return teams; + return teams as Array; } - async _queryProjectUnShare(): Promise { + async _queryProjectUnShare() { await this._runGraphQL( ` mutation ($id: ID!) { @@ -1045,8 +1040,11 @@ export default class VCS { async _queryProjectShare( teamId: string, - keys: Array<{ accountId: string, encSymmetricKey: string }>, - ): Promise { + keys: Array<{ + accountId: string; + encSymmetricKey: string; + }>, + ) { await this._runGraphQL( ` mutation ($id: ID!, $teamId: ID!, $keys: [ProjectShareKeyInput!]!) { @@ -1067,14 +1065,14 @@ export default class VCS { async _queryProjectShareInstructions( teamId: string, ): Promise<{ - teamId: string, + teamId: string; projectKey: { - encSymmetricKey: string, - }, + encSymmetricKey: string; + }; memberKeys: Array<{ - accountId: string, - publicKey: string, - }>, + accountId: string; + publicKey: string; + }>; }> { const { projectShareInstructions } = await this._runGraphQL( ` @@ -1097,7 +1095,6 @@ export default class VCS { }, 'projectShareInstructions', ); - return projectShareInstructions; } @@ -1115,7 +1112,6 @@ export default class VCS { {}, 'projects', ); - return projects; } @@ -1135,7 +1131,6 @@ export default class VCS { }, 'project', ); - return project; } @@ -1164,16 +1159,14 @@ export default class VCS { return project.teams; } - async _queryCreateProject(workspaceId: string, workspaceName: string): Promise { + async _queryCreateProject(workspaceId: string, workspaceName: string) { const { publicKey } = this._assertSession(); // Generate symmetric key for ResourceGroup const symmetricKey = await crypt.generateAES256Key(); const symmetricKeyStr = JSON.stringify(symmetricKey); - // Encrypt the symmetric key with Account public key const encSymmetricKey = crypt.encryptRSAWithJWK(publicKey, symmetricKeyStr); - const { projectCreate } = await this._runGraphQL( ` mutation ($rootDocumentId: ID!, $name: String!, $id: ID!, $key: String!) { @@ -1192,10 +1185,8 @@ export default class VCS { }, 'switchAndCreateProjectIfNotExist', ); - console.log(`[sync] Created remote project ${projectCreate.id} (${projectCreate.name})`); - - return projectCreate; + return projectCreate as Project; } async _getProject(): Promise { @@ -1207,15 +1198,17 @@ export default class VCS { return this._store.getItem(paths.project(id)); } - async _getProjectSymmetricKey(): Promise { + async _getProjectSymmetricKey() { const { privateKey } = this._assertSession(); + const encSymmetricKey = await this._queryProjectKey(); const symmetricKeyStr = crypt.decryptRSAWithJWK(privateKey, encSymmetricKey); return JSON.parse(symmetricKeyStr); } - async _assertProject(): Promise { + async _assertProject() { const project = await this._getProject(); + if (project === null) { throw new Error('Failed to find local project id=' + this._projectId()); } @@ -1223,12 +1216,13 @@ export default class VCS { return project; } - async _storeProject(project: Project): Promise { + async _storeProject(project: Project) { return this._store.setItem(paths.project(project.id), project); } async _getHead(): Promise { const head = await this._store.getItem(paths.head(this._projectId())); + if (head === null) { await this._storeHead({ branch: 'master' }); return this._getHead(); @@ -1237,17 +1231,12 @@ export default class VCS { return head; } - async _getCurrentBranch(): Promise { + async _getCurrentBranch() { const head = await this._getHead(); return this._getOrCreateBranch(head.branch); } - _assertSession(): {| - accountId: string, - sessionId: string, - privateKey: Object, - publicKey: Object, - |} { + _assertSession() { if (!session.isLoggedIn()) { throw new Error('Not logged in'); } @@ -1260,8 +1249,9 @@ export default class VCS { }; } - async _assertBranch(branchName: string): Promise { + async _assertBranch(branchName: string) { const branch = await this._getBranch(branchName); + if (branch === null) { throw new Error(`Branch does not exist with name ${branchName}`); } @@ -1269,7 +1259,7 @@ export default class VCS { return branch; } - _projectId(): string { + _projectId() { if (this._project === null) { throw new Error('No active project'); } @@ -1279,15 +1269,19 @@ export default class VCS { async _getBranch(name: string, projectId?: string): Promise { const pId = projectId || this._projectId(); + const p = paths.branch(pId, name); return this._store.getItem(p); } - async _getBranches(projectId?: string): Promise> { - const branches = []; + async _getBranches(projectId?: string) { + const branches: Array = []; + const pId = projectId || this._projectId(); + for (const p of await this._store.keys(paths.branches(pId))) { const b = await this._store.getItem(p); + if (b === null) { // Should never happen throw new Error(`Failed to get branch path=${p}`); @@ -1313,14 +1307,13 @@ export default class VCS { modified: new Date(), snapshots: [], }); - return this._getOrCreateBranch(name); } return branch; } - async _getProjectByRootDocument(rootDocumentId: string): Promise { + async _getProjectByRootDocument(rootDocumentId: string) { if (!rootDocumentId) { throw new Error('No root document ID supplied for project'); } @@ -1333,6 +1326,7 @@ export default class VCS { if (matchedProjects.length > 1) { for (const p of matchedProjects) { const branches = await this._getBranches(p.id); + if (!branches.find(b => b.snapshots.length > 0)) { await this._removeProject(p); matchedProjects = matchedProjects.filter(({ id }) => id !== p.id); @@ -1354,13 +1348,17 @@ export default class VCS { return matchedProjects[0] || null; } - async _getOrCreateProjectByRootDocument(rootDocumentId: string, name: string): Promise { + async _getOrCreateProjectByRootDocument(rootDocumentId: string, name: string) { let project: Project | null = await this._getProjectByRootDocument(rootDocumentId); // If we still don't have a project, create one if (!project) { const id = generateId('prj'); - project = { id, name, rootDocumentId }; + project = { + id, + name, + rootDocumentId, + }; await this._storeProject(project); console.log(`[sync] Created project ${project.id}`); } @@ -1368,13 +1366,15 @@ export default class VCS { return project; } - async _allProjects(): Promise> { - const projects = []; + async _allProjects() { + const projects: Array = []; const basePath = paths.projects(); const keys = await this._store.keys(basePath, false); + for (const key of keys) { const id = path.basename(key); const p: Project | null = await this._getProjectById(id); + if (p === null) { // Folder exists but project meta file is gone continue; @@ -1386,8 +1386,9 @@ export default class VCS { return projects; } - async _assertSnapshot(id: string): Promise { - const snapshot = await this._store.getItem(paths.snapshot(this._projectId(), id)); + async _assertSnapshot(id: string) { + const snapshot: Snapshot = await this._store.getItem(paths.snapshot(this._projectId(), id)); + if (snapshot && typeof snapshot.created === 'string') { snapshot.created = new Date(snapshot.created); } @@ -1399,8 +1400,9 @@ export default class VCS { return snapshot; } - async _getSnapshot(id: string): Promise { - const snapshot = await this._store.getItem(paths.snapshot(this._projectId(), id)); + async _getSnapshot(id: string) { + const snapshot: Snapshot = await this._store.getItem(paths.snapshot(this._projectId(), id)); + if (snapshot && typeof snapshot.created === 'string') { snapshot.created = new Date(snapshot.created); } @@ -1408,29 +1410,33 @@ export default class VCS { return snapshot; } - async _getLatestSnapshot(branchName: string): Promise { + async _getLatestSnapshot(branchName: string) { const branch = await this._getOrCreateBranch(branchName); const snapshots = branch ? branch.snapshots : []; const parentId = snapshots.length ? snapshots[snapshots.length - 1] : EMPTY_HASH; return this._getSnapshot(parentId); } - async _storeSnapshot(snapshot: Snapshot): Promise { + async _storeSnapshot(snapshot: Snapshot) { return this._store.setItem(paths.snapshot(this._projectId(), snapshot.id), snapshot); } - async _storeSnapshots(snapshots: Array): Promise { - const promises = []; + async _storeSnapshots(snapshots: Array) { + const promises: Array> = []; + for (const snapshot of snapshots) { const p = paths.snapshot(this._projectId(), snapshot.id); - promises.push(this._store.setItem(p, snapshot)); + const promise = this._store.setItem(p, snapshot); + // @ts-expect-error -- TSCONVERSION appears to be a genuine error + promises.push(promise); } await Promise.all(promises); } - async _storeBranch(branch: Branch): Promise { + async _storeBranch(branch: Branch) { const errMsg = VCS.validateBranchName(branch.name); + if (errMsg) { throw new Error(errMsg); } @@ -1439,26 +1445,27 @@ export default class VCS { return this._store.setItem(paths.branch(this._projectId(), branch.name.toLowerCase()), branch); } - async _removeBranch(branch: Branch): Promise { + async _removeBranch(branch: Branch) { return this._store.removeItem(paths.branch(this._projectId(), branch.name)); } - async _removeProject(project: Project): Promise { + async _removeProject(project: Project) { console.log(`[sync] Remove local project ${project.id}`); return this._store.removeItem(paths.project(project.id)); } - async _storeHead(head: Head): Promise { + async _storeHead(head: Head) { await this._store.setItem(paths.head(this._projectId()), head); } - async _getBlob(id: string): Promise { + async _getBlob(id: string) { const p = paths.blob(this._projectId(), id); - return this._store.getItem(p); + return this._store.getItem(p) as Record | null; } - async _getBlobs(ids: Array): Promise> { - const promises = []; + async _getBlobs(ids: Array) { + const promises: Array | null>> = []; + for (const id of ids) { promises.push(this._getBlob(id)); } @@ -1466,12 +1473,13 @@ export default class VCS { return Promise.all(promises); } - async _storeBlob(id: string, content: Object | null): Promise { + async _storeBlob(id: string, content: Record | null) { return this._store.setItem(paths.blob(this._projectId(), id), content); } - async _storeBlobs(map: { [string]: string }): Promise { - const promises = []; + async _storeBlobs(map: Record) { + const promises: Array> = []; + for (const id of Object.keys(map)) { const buff = Buffer.from(map[id], 'utf8'); promises.push(this._storeBlob(id, buff)); @@ -1480,8 +1488,9 @@ export default class VCS { await Promise.all(promises); } - async _storeBlobsBuffer(map: { [string]: Buffer }): Promise { - const promises = []; + async _storeBlobsBuffer(map: Record) { + const promises: Array> = []; + for (const id of Object.keys(map)) { const p = paths.blob(this._projectId(), id); promises.push(this._store.setItemRaw(p, map[id])); @@ -1490,15 +1499,15 @@ export default class VCS { await Promise.all(promises); } - async _getBlobRaw(id: string): Promise { + async _getBlobRaw(id: string) { return this._store.getItemRaw(paths.blob(this._projectId(), id)); } - async _hasBlob(id: string): Promise { + async _hasBlob(id: string) { return this._store.hasItem(paths.blob(this._projectId(), id)); } - async _queryProjectArchive(projectId: string): Promise { + async _queryProjectArchive(projectId: string) { await this._runGraphQL( ` mutation ($id: ID!) { @@ -1510,18 +1519,13 @@ export default class VCS { }, 'projectArchive', ); - console.log(`[sync] Archived remote project ${projectId}`); } } - /** Generate snapshot ID from hashing parent, project, and state together */ -function _generateSnapshotID(parentId: string, projectId: string, state: SnapshotState): string { - const hash = crypto - .createHash('sha1') - .update(projectId) - .update(parentId); +function _generateSnapshotID(parentId: string, projectId: string, state: SnapshotState) { + const hash = crypto.createHash('sha1').update(projectId).update(parentId); const newState = [...state].sort((a, b) => (a.blob > b.blob ? 1 : -1)); for (const entry of newState) { diff --git a/packages/insomnia-app/app/sync/vcs/paths.js b/packages/insomnia-app/app/sync/vcs/paths.js deleted file mode 100644 index e191c0558e..0000000000 --- a/packages/insomnia-app/app/sync/vcs/paths.js +++ /dev/null @@ -1,42 +0,0 @@ -// @flow - -export function projects(): string { - return '/projects/'; -} - -export function projectBase(projectId: string): string { - return `${projects()}${projectId}/`; -} - -export function head(projectId: string): string { - return `${projectBase(projectId)}head.json`; -} - -export function project(projectId: string): string { - return `${projectBase(projectId)}meta.json`; -} - -export function blobs(projectId: string): string { - return `${projectBase(projectId)}blobs/`; -} - -export function blob(projectId: string, blobId: string): string { - const subPath = `${blobId.slice(0, 2)}/${blobId.slice(2)}`; - return `${blobs(projectId)}${subPath}`; -} - -export function snapshots(projectId: string): string { - return `${projectBase(projectId)}snapshots/`; -} - -export function snapshot(projectId: string, snapshotId: string): string { - return `${snapshots(projectId)}${snapshotId}.json`; -} - -export function branches(projectId: string): string { - return `${projectBase(projectId)}branches/`; -} - -export function branch(projectId: string, branchName: string): string { - return `${branches(projectId)}${branchName}.json`; -} diff --git a/packages/insomnia-app/app/sync/vcs/paths.ts b/packages/insomnia-app/app/sync/vcs/paths.ts new file mode 100644 index 0000000000..0a1324eafe --- /dev/null +++ b/packages/insomnia-app/app/sync/vcs/paths.ts @@ -0,0 +1,41 @@ + +export function projects() { + return '/projects/'; +} + +export function projectBase(projectId: string) { + return `${projects()}${projectId}/`; +} + +export function head(projectId: string) { + return `${projectBase(projectId)}head.json`; +} + +export function project(projectId: string) { + return `${projectBase(projectId)}meta.json`; +} + +export function blobs(projectId: string) { + return `${projectBase(projectId)}blobs/`; +} + +export function blob(projectId: string, blobId: string) { + const subPath = `${blobId.slice(0, 2)}/${blobId.slice(2)}`; + return `${blobs(projectId)}${subPath}`; +} + +export function snapshots(projectId: string) { + return `${projectBase(projectId)}snapshots/`; +} + +export function snapshot(projectId: string, snapshotId: string) { + return `${snapshots(projectId)}${snapshotId}.json`; +} + +export function branches(projectId: string) { + return `${projectBase(projectId)}branches/`; +} + +export function branch(projectId: string, branchName: string) { + return `${branches(projectId)}${branchName}.json`; +} diff --git a/packages/insomnia-app/app/sync/vcs/util.js b/packages/insomnia-app/app/sync/vcs/util.ts similarity index 86% rename from packages/insomnia-app/app/sync/vcs/util.js rename to packages/insomnia-app/app/sync/vcs/util.ts index 6ba968a2d1..8c74cb0f54 100644 --- a/packages/insomnia-app/app/sync/vcs/util.js +++ b/packages/insomnia-app/app/sync/vcs/util.ts @@ -1,4 +1,3 @@ -// @flow import clone from 'clone'; import crypto from 'crypto'; import { deterministicStringify } from '../lib/deterministicStringify'; @@ -14,7 +13,6 @@ import type { StatusCandidate, StatusCandidateMap, } from '../types'; - // Keys for VCS to ignore when computing changes const IGNORED_KEYS = ['modified']; @@ -32,24 +30,29 @@ export function generateStateMap(state: SnapshotState | null): SnapshotStateMap } const map = {}; + for (const entry of state) { map[entry.key] = entry; } + return map; } export function generateCandidateMap(candidates: Array): StatusCandidateMap { const map = {}; + for (const candidate of candidates) { map[candidate.key] = candidate; } + return map; } -export function combinedMapKeys( +export function combinedMapKeys( ...maps: Array ): Array { const keyMap = {}; + for (const map of maps) { for (const key of Object.keys(map)) { keyMap[key] = true; @@ -63,12 +66,14 @@ export function threeWayMerge( root: SnapshotState, trunk: SnapshotState, other: SnapshotState, -): { state: SnapshotState, conflicts: Array } { +): { + state: SnapshotState; + conflicts: Array; +} { const stateRoot = generateStateMap(root); const stateTrunk = generateStateMap(trunk); const stateOther = generateStateMap(other); const allKeys = combinedMapKeys(stateRoot, stateTrunk, stateOther); - const newState: SnapshotState = []; const conflicts: Array = []; @@ -87,11 +92,9 @@ export function threeWayMerge( // other => [ exists, missing, modified ] // // Therefore, the total number of cases is equal to 2 * 3! = 12 - // ~~~~~~~~~~ // // Unmodified // // ~~~~~~~~~~ // - // (1/12) // Unmodified if (root && trunk && other && root.blob === trunk.blob && root.blob === other.blob) { @@ -102,7 +105,6 @@ export function threeWayMerge( // ~~~~~~~~~ // // Deletions // // ~~~~~~~~~ // - // (2/12) // Deleted in both if (root && trunk === null && other === null) { @@ -124,7 +126,6 @@ export function threeWayMerge( // ~~~~~~~~~ // // Additions // // ~~~~~~~~~ // - // (5/12) // Added in both if (root === null && trunk && other) { @@ -160,7 +161,6 @@ export function threeWayMerge( // ~~~~~~~~~~~~~ // // Modifications // // ~~~~~~~~~~~~~ // - // (8/12) // Modified in both if (root && trunk && other && root.blob !== trunk.blob && root.blob !== other.blob) { @@ -196,7 +196,6 @@ export function threeWayMerge( // ~~~~~~ // // Combos // // ~~~~~~ // - // (11/12) // Deleted in trunk and modified in other if (root && trunk === null && other && other.blob !== root.blob) { @@ -240,12 +239,14 @@ export function threeWayMerge( export function compareBranches( a: Branch | null, b: Branch | null, -): { ahead: number, behind: number } { +): { + ahead: number; + behind: number; +} { const snapshotsA = a ? a.snapshots : []; const snapshotsB = b ? b.snapshots : []; const latestA = snapshotsA[snapshotsA.length - 1] || null; const latestB = snapshotsB[snapshotsB.length - 1] || null; - const result = { ahead: 0, behind: 0, @@ -266,33 +267,33 @@ export function compareBranches( } const root = getRootSnapshot(a, b); + if (root === null) { return result; } const indexOfRootInA = snapshotsA.indexOf(root); const indexOfRootInB = snapshotsB.indexOf(root); - result.ahead = snapshotsA.length - indexOfRootInA - 1; result.behind = snapshotsB.length - indexOfRootInB - 1; - return result; } +export interface StateDelta { + add: Array; + update: Array; + remove: Array; +} + export function stateDelta( base: SnapshotState, desired: SnapshotState, -): { - add: Array, - update: Array, - remove: Array, -} { - const result = { +) { + const result: StateDelta = { add: [], update: [], remove: [], }; - const stateMapStart = generateStateMap(base); const stateMapFinish = generateStateMap(desired); @@ -319,14 +320,12 @@ export function stateDelta( return result; } -export function getStagable( - state: SnapshotState, - candidates: Array, -): Array { +export function getStagable(state: SnapshotState, candidates: Array) { const stagable: Array = []; - const stateMap = generateStateMap(state); const candidateMap = generateCandidateMap(candidates); + + // @ts-expect-error -- TSCONVERSION need to fix the index signatures for (const key of combinedMapKeys(stateMap, candidateMap)) { const entry = stateMap[key]; const candidate = candidateMap[key]; @@ -334,22 +333,41 @@ export function getStagable( if (!entry && candidate) { const { name, document } = candidate; const { hash: blobId, content: blobContent } = hashDocument(document); - stagable.push({ key, name, blobId, blobContent, added: true }); + stagable.push({ + key, + name, + blobId, + blobContent, + added: true, + }); continue; } if (entry && !candidate) { const { name, blob: blobId } = entry; - stagable.push({ key, name, blobId, deleted: true }); + stagable.push({ + key, + name, + blobId, + deleted: true, + }); continue; } if (entry && candidate) { const { document, name } = candidate; const { hash: blobId, content: blobContent } = hashDocument(document); + if (entry.blob !== blobId) { - stagable.push({ key, name, blobId, blobContent, modified: true }); + stagable.push({ + key, + name, + blobId, + blobContent, + modified: true, + }); } + continue; } } @@ -360,8 +378,8 @@ export function getStagable( export function getRootSnapshot(a: Branch | null, b: Branch | null): string | null { const snapshotsA = a ? a.snapshots : []; const snapshotsB = b ? b.snapshots : []; - const rootSnapshotId = ''; + for (let ai = snapshotsA.length - 1; ai >= 0; ai--) { for (let bi = snapshotsB.length - 1; bi >= 0; bi--) { if (snapshotsA[ai] === snapshotsB[bi]) { @@ -369,6 +387,7 @@ export function getRootSnapshot(a: Branch | null, b: Branch | null): string | nu } } } + return rootSnapshotId || null; } @@ -376,15 +395,9 @@ export function preMergeCheck( trunkState: SnapshotState, otherState: SnapshotState, candidates: Array, -): { - conflicts: Array, - dirty: Array, -} { - const result = { - conflicts: [], - dirty: [], - }; - +) { + const conflicts: Array = []; + const dirty: Array = []; const trunkMap = generateStateMap(trunkState); const otherMap = generateStateMap(otherState); @@ -395,7 +408,7 @@ export function preMergeCheck( // Candidate is not in trunk or other (not yet in version control) if (!trunk && !other) { - result.dirty.push(candidate); + dirty.push(candidate); continue; } @@ -424,54 +437,68 @@ export function preMergeCheck( blobId !== other.blob && blobId !== trunk.blob ) { - result.dirty.push(candidate); + dirty.push(candidate); continue; } // All other cases result in conflict - result.conflicts.push(candidate); + conflicts.push(candidate); } - return result; + return { + conflicts, + dirty, + }; } -export function hashDocument(doc: Object): { content: string, hash: string } { +export function hashDocument( + doc: Record, +): { + content: string; + hash: string; +} { if (!doc) { throw new Error('Cannot hash undefined value'); } // Remove fields we don't care about for sync purposes const newDoc = clone(doc); + for (const key of IGNORED_KEYS) { delete newDoc[key]; } const content = deterministicStringify(newDoc); - const hash = crypto - .createHash('sha1') - .update(content) - .digest('hex'); - - return { hash, content }; + const hash = crypto.createHash('sha1').update(content).digest('hex'); + return { + hash, + content, + }; } -export function updateStateWithConflictResolutions( - state: SnapshotState, - conflicts: Array, -): SnapshotState { +export function updateStateWithConflictResolutions(state: SnapshotState, conflicts: Array) { const newStateMap = generateStateMap(state); + for (const { choose, key, name } of conflicts) { const stateEntry = state.find(e => e.key === key); // Not in the state, but we choose the conflict if (!stateEntry && choose !== null) { - newStateMap[key] = { key, name, blob: choose }; + newStateMap[key] = { + key, + name, + blob: choose, + }; continue; } // Add the conflict if (choose !== null) { - newStateMap[key] = { ...(stateEntry: Object), blob: choose }; + // @ts-expect-error -- TSCONVERSION need to decided how to reorder this + newStateMap[key] = { + ...stateEntry, + blob: choose, + }; continue; } @@ -490,8 +517,9 @@ export function describeChanges(a: any, b: any): Array { return []; } - const changes = []; + const changes: Array = []; const allKeys = [...Object.keys({ ...a, ...b })]; + for (const key of allKeys) { if (IGNORED_KEYS.includes(key)) { continue; @@ -499,7 +527,6 @@ export function describeChanges(a: any, b: any): Array { const aValue = a[key]; const bValue = b[key]; - const aStr = deterministicStringify(aValue); const bStr = deterministicStringify(bValue); diff --git a/packages/insomnia-app/app/templating/__tests__/utils.test.js b/packages/insomnia-app/app/templating/__tests__/utils.test.ts similarity index 58% rename from packages/insomnia-app/app/templating/__tests__/utils.test.js rename to packages/insomnia-app/app/templating/__tests__/utils.test.ts index a079951b4b..b11d664e1c 100644 --- a/packages/insomnia-app/app/templating/__tests__/utils.test.js +++ b/packages/insomnia-app/app/templating/__tests__/utils.test.ts @@ -3,29 +3,69 @@ import { globalBeforeEach } from '../../__jest__/before-each'; describe('getKeys()', () => { beforeEach(globalBeforeEach); + it('flattens complex object', () => { const obj = { foo: 'bar', - nested: { a: { b: {} } }, + nested: { + a: { + b: {}, + }, + }, null: null, undefined: undefined, false: false, - array: ['hello', { hi: 'there' }, true, ['x', 'y', 'z']], + array: [ + 'hello', + { + hi: 'there', + }, + true, + ['x', 'y', 'z'], + ], }; - const keys = utils.getKeys(obj).sort((a, b) => (a.name > b.name ? 1 : -1)); - expect(keys).toEqual([ - { name: 'array[0]', value: obj.array[0] }, - { name: 'array[1].hi', value: obj.array[1].hi }, - { name: 'array[2]', value: obj.array[2] }, - { name: 'array[3][0]', value: obj.array[3][0] }, - { name: 'array[3][1]', value: obj.array[3][1] }, - { name: 'array[3][2]', value: obj.array[3][2] }, - { name: 'false', value: obj.false }, - { name: 'foo', value: obj.foo }, - { name: 'null', value: obj.null }, - { name: 'undefined', value: obj.undefined }, + { + name: 'array[0]', + value: obj.array[0], + }, + { + name: 'array[1].hi', + value: obj.array[1].hi, + }, + { + name: 'array[2]', + value: obj.array[2], + }, + { + name: 'array[3][0]', + value: obj.array[3][0], + }, + { + name: 'array[3][1]', + value: obj.array[3][1], + }, + { + name: 'array[3][2]', + value: obj.array[3][2], + }, + { + name: 'false', + value: obj.false, + }, + { + name: 'foo', + value: obj.foo, + }, + { + name: 'null', + value: obj.null, + }, + { + name: 'undefined', + value: obj.undefined, + }, ]); }); @@ -36,93 +76,129 @@ describe('getKeys()', () => { // Nothing }, }; - const keys = utils.getKeys(obj); - expect(keys).toEqual([{ name: 'foo', value: 'bar' }]); + expect(keys).toEqual([ + { + name: 'foo', + value: 'bar', + }, + ]); }); }); describe('tokenizeTag()', () => { beforeEach(globalBeforeEach); + it('tokenizes complex tag', () => { const actual = utils.tokenizeTag('{% name bar, "baz \\"qux\\"" , 1 + 5 | default("foo") %}'); - const expected = { name: 'name', args: [ - { type: 'variable', value: 'bar' }, - { type: 'string', value: 'baz "qux"', quotedBy: '"' }, - { type: 'expression', value: '1 + 5 | default("foo")' }, + { + type: 'variable', + value: 'bar', + }, + { + type: 'string', + value: 'baz "qux"', + quotedBy: '"', + }, + { + type: 'expression', + value: '1 + 5 | default("foo")', + }, ], }; - expect(actual).toEqual(expected); }); it('handles whitespace', () => { const minimal = utils.tokenizeTag("{%name'foo',bar%}"); const generous = utils.tokenizeTag("{%name \t'foo' , bar\t\n%}"); - const expected = { name: 'name', args: [ - { type: 'string', value: 'foo', quotedBy: "'" }, - { type: 'variable', value: 'bar' }, + { + type: 'string', + value: 'foo', + quotedBy: "'", + }, + { + type: 'variable', + value: 'bar', + }, ], }; - expect(minimal).toEqual(expected); expect(generous).toEqual(expected); }); it('handles type string', () => { const actual = utils.tokenizeTag("{% name 'foo' %}"); - const expected = { name: 'name', - args: [{ type: 'string', value: 'foo', quotedBy: "'" }], + args: [ + { + type: 'string', + value: 'foo', + quotedBy: "'", + }, + ], }; - expect(actual).toEqual(expected); }); it('handles type number', () => { const actual = utils.tokenizeTag('{% name 9.324, 8, 7 %}'); - const expected = { name: 'name', args: [ - { type: 'number', value: '9.324' }, - { type: 'number', value: '8' }, - { type: 'number', value: '7' }, + { + type: 'number', + value: '9.324', + }, + { + type: 'number', + value: '8', + }, + { + type: 'number', + value: '7', + }, ], }; - expect(actual).toEqual(expected); }); it('handles type boolean', () => { const actual = utils.tokenizeTag('{% name true, false %}'); - const expected = { name: 'name', args: [ - { type: 'boolean', value: true }, - { type: 'boolean', value: false }, + { + type: 'boolean', + value: true, + }, + { + type: 'boolean', + value: false, + }, ], }; - expect(actual).toEqual(expected); }); it('handles type expression', () => { const actual = utils.tokenizeTag("{% name 5 * 10 + 'hello' | default(2 - 3) %}"); - const expected = { name: 'name', - args: [{ type: 'expression', value: "5 * 10 + 'hello' | default(2 - 3)" }], + args: [ + { + type: 'expression', + value: "5 * 10 + 'hello' | default(2 - 3)", + }, + ], }; - expect(actual).toEqual(expected); }); @@ -132,24 +208,26 @@ describe('tokenizeTag()', () => { */ it('handles no commas', () => { const actual = utils.tokenizeTag('{% name foo bar baz %}'); - const expected = { name: 'name', - args: [{ type: 'expression', value: 'foo bar baz' }], + args: [ + { + type: 'expression', + value: 'foo bar baz', + }, + ], }; - expect(actual).toEqual(expected); }); }); describe('unTokenizeTag()', () => { beforeEach(globalBeforeEach); + it('handles the default case', () => { const tagStr = '{% name bar, "baz \\"qux\\"" , 1 + 5, \'hi\' %}'; - const tagData = utils.tokenizeTag(tagStr); const result = utils.unTokenizeTag(tagData); - expect(result).toEqual('{% name bar, "baz \\"qux\\"", 1 + 5, \'hi\' %}'); }); @@ -157,19 +235,41 @@ describe('unTokenizeTag()', () => { const tagData = { name: 'name', args: [ - { type: 'boolean', value: 'true' }, - { type: 'enum', value: 'foo' }, - { type: 'expression', value: 'foo.length' }, - { type: 'file', value: 'foo/bar/baz' }, - { type: 'model', value: 'id_123' }, - { type: 'number', value: '10' }, - { type: 'string', value: 'foo' }, - { type: 'variable', value: 'var' }, + { + type: 'boolean', + value: 'true', + }, + { + type: 'enum', + value: 'foo', + }, + { + type: 'expression', + value: 'foo.length', + }, + { + type: 'file', + value: 'foo/bar/baz', + }, + { + type: 'model', + value: 'id_123', + }, + { + type: 'number', + value: '10', + }, + { + type: 'string', + value: 'foo', + }, + { + type: 'variable', + value: 'var', + }, ], }; - const result = utils.unTokenizeTag(tagData); - expect(result).toEqual( "{% name true, 'foo', foo.length, 'foo/bar/baz', 'id_123', 10, 'foo', var %}", ); @@ -179,19 +279,24 @@ describe('unTokenizeTag()', () => { const tagData = { name: 'name', args: [ - { type: 'file', value: 'foo/bar/baz' }, - { type: 'model', value: 'foo' }, + { + type: 'file', + value: 'foo/bar/baz', + }, + { + type: 'model', + value: 'foo', + }, ], }; - const result = utils.unTokenizeTag(tagData); - expect(result).toEqual("{% name 'foo/bar/baz', 'foo' %}"); }); }); describe('encodeEncoding()', () => { beforeEach(globalBeforeEach); + it('encodes things', () => { expect(utils.encodeEncoding('hello', 'base64')).toBe('b64::aGVsbG8=::46b'); expect(utils.encodeEncoding(null, 'base64')).toBe(null); @@ -203,6 +308,7 @@ describe('encodeEncoding()', () => { describe('decodeEncoding()', () => { beforeEach(globalBeforeEach); + it('encodes things', () => { expect(utils.decodeEncoding('b64::aGVsbG8=::46b')).toBe('hello'); expect(utils.decodeEncoding('aGVsbG8=')).toBe('aGVsbG8='); diff --git a/packages/insomnia-app/app/templating/base-extension.js b/packages/insomnia-app/app/templating/base-extension.ts similarity index 72% rename from packages/insomnia-app/app/templating/base-extension.js rename to packages/insomnia-app/app/templating/base-extension.ts index 3fa163b166..fb9f8b076b 100644 --- a/packages/insomnia-app/app/templating/base-extension.js +++ b/packages/insomnia-app/app/templating/base-extension.ts @@ -1,67 +1,77 @@ import * as models from '../models/index'; import * as templating from './index'; import * as pluginContexts from '../plugins/context'; -import * as db from '../common/database'; +import { database as db } from '../common/database'; import { decodeEncoding } from './utils'; +import { PluginTemplateTag } from './extensions'; const EMPTY_ARG = '__EMPTY_NUNJUCKS_ARG__'; export default class BaseExtension { - constructor(ext, plugin) { + _ext: PluginTemplateTag | null = null; + _plugin: Plugin | null = null; + tags: Array | null = null; + + constructor(ext: PluginTemplateTag, plugin: Plugin) { this._ext = ext; this._plugin = plugin; - this.tags = [this.getTag()]; + const tag = this.getTag(); + this.tags = [ + ...(tag === null ? [] : [tag]), + ]; } getTag() { - return this._ext.name; + return this._ext?.name || null; } getPriority() { - return this._ext.priority || -1; + return this._ext?.priority || -1; } getName() { - return this._ext.displayName || this.getTag(); + return this._ext?.displayName || this.getTag(); } getDescription() { - return this._ext.description || 'no description'; + return this._ext?.description || 'no description'; } getLiveDisplayName() { return ( - this._ext.liveDisplayName || - function(args) { + // @ts-expect-error -- TSCONVERSION + this._ext?.liveDisplayName || + function() { return ''; } ); } getDisablePreview() { - return this._ext.disablePreview || (() => false); + return this._ext?.disablePreview || (() => false); } getArgs() { - return this._ext.args || []; + return this._ext?.args || []; } getActions() { - return this._ext.actions || []; + return this._ext?.actions || []; } isDeprecated() { - return this._ext.deprecated || false; + return this._ext?.deprecated || false; } run(...args) { - return this._ext.run(...args); + // @ts-expect-error -- TSCONVERSION + return this._ext?.run(...args); } parse(parser, nodes, lexer) { const tok = parser.nextToken(); - let args; + if (parser.peekToken().type !== lexer.TOKEN_BLOCK_END) { args = parser.parseSignature(null, true); } else { @@ -77,32 +87,31 @@ export default class BaseExtension { asyncRun({ ctx: renderContext }, ...runArgs) { // Pull the callback off the end const callback = runArgs[runArgs.length - 1]; - // Pull out the meta helper const renderMeta = renderContext.getMeta ? renderContext.getMeta() : {}; - // Pull out the purpose const renderPurpose = renderContext.getPurpose ? renderContext.getPurpose() : null; - // Pull out the environment ID const environmentId = renderContext.getEnvironmentId ? renderContext.getEnvironmentId() : 'n/a'; - // Extract the rest of the args const args = runArgs .slice(0, runArgs.length - 1) .filter(a => a !== EMPTY_ARG) .map(decodeEncoding); - // Define a helper context with utils const helperContext = { ...pluginContexts.app.init(renderPurpose), + // @ts-expect-error -- TSCONVERSION ...pluginContexts.store.init(this._plugin), ...pluginContexts.network.init(environmentId), context: renderContext, meta: renderMeta, renderPurpose, util: { - render: str => templating.render(str, { context: renderContext }), + render: str => + templating.render(str, { + context: renderContext, + }), models: { request: { getById: models.request.getById, @@ -114,8 +123,12 @@ export default class BaseExtension { return ancestors.filter(doc => doc._id !== request._id); }, }, - workspace: { getById: models.workspace.getById }, - oAuth2Token: { getByRequestId: models.oAuth2Token.getByParentId }, + workspace: { + getById: models.workspace.getById, + }, + oAuth2Token: { + getByRequestId: models.oAuth2Token.getByParentId, + }, cookieJar: { getOrCreateForWorkspace: workspace => { return models.cookieJar.getOrCreateForParentId(workspace._id); @@ -128,8 +141,8 @@ export default class BaseExtension { }, }, }; - let result; + try { result = this.run(helperContext, ...args); } catch (err) { diff --git a/packages/insomnia-app/app/templating/extensions/index.js b/packages/insomnia-app/app/templating/extensions/index.js deleted file mode 100644 index dc1d93a21d..0000000000 --- a/packages/insomnia-app/app/templating/extensions/index.js +++ /dev/null @@ -1,100 +0,0 @@ -// @flow -import type { NunjucksParsedTagArg } from '../utils'; -import type { Request } from '../../models/request'; -import type { Response } from '../../models/response'; -import type { PluginStore } from '../../plugins/context'; - -export type PluginArgumentValue = string | number | boolean; -type DisplayName = string | ((args: Array) => string); - -type PluginArgumentBase = { - displayName: DisplayName, - description?: string, - help?: string, - hide?: (args: Array) => boolean, -}; - -export type PluginArgumentEnumOption = { - displayName: DisplayName, - value: PluginArgumentValue, - description?: string, - placeholder?: string, -}; - -export type PluginArgumentEnum = PluginArgumentBase & { - type: 'enum', - options: Array, - defaultValue?: PluginArgumentValue, -}; - -export type PluginArgumentModel = PluginArgumentBase & { - type: 'model', - model: string, - defaultValue?: string, -}; - -export type PluginArgumentString = PluginArgumentBase & { - type: 'string', - placeholder?: string, - defaultValue?: string, -}; - -export type PluginArgumentBoolean = PluginArgumentBase & { - type: 'boolean', - defaultValue?: boolean, -}; - -export type PluginArgumentFile = PluginArgumentBase & { - type: 'file', -}; - -export type PluginArgumentNumber = PluginArgumentBase & { - type: 'number', - placeholder?: string, - defaultValue?: number, -}; - -export type PluginArgument = - | PluginArgumentEnum - | PluginArgumentModel - | PluginArgumentString - | PluginArgumentBoolean - | PluginArgumentFile - | PluginArgumentNumber; - -export type PluginTemplateTagContext = { - util: { - models: { - request: { - getById: (id: string) => Promise, - }, - response: { - getLatestForRequestId: (id: string) => Promise, - getBodyBuffer: (response: Response, fallback?: any) => Promise, - }, - }, - }, -}; - -export type PluginTemplateTagActionContext = { - store: PluginStore, -}; - -export type PluginTemplateTagAction = { - name: string, - icon?: string, - run: (context: PluginTemplateTagActionContext) => Promise, -}; - -export type PluginTemplateTag = { - args: Array, - name: string, - displayName: DisplayName, - disablePreview: () => boolean, - description: string, - actions: Array, - run: (context: PluginTemplateTagContext, ...arg: Array) => Promise | any, - deprecated?: boolean, - validate?: (value: any) => ?string, - priority?: number, -}; diff --git a/packages/insomnia-app/app/templating/extensions/index.ts b/packages/insomnia-app/app/templating/extensions/index.ts new file mode 100644 index 0000000000..12e5d81484 --- /dev/null +++ b/packages/insomnia-app/app/templating/extensions/index.ts @@ -0,0 +1,100 @@ +import type { NunjucksParsedTagArg } from '../utils'; +import type { Request } from '../../models/request'; +import type { Response } from '../../models/response'; +import type { PluginStore } from '../../plugins/context'; + +export type PluginArgumentValue = string | number | boolean; + +type DisplayName = string | ((args: Array) => string); + +interface PluginArgumentBase { + displayName: DisplayName; + description?: string; + help?: string; + hide?: (args: Array) => boolean; +} + +export interface PluginArgumentEnumOption { + displayName: DisplayName; + value: PluginArgumentValue; + description?: string; + placeholder?: string; +} + +export type PluginArgumentEnum = PluginArgumentBase & { + type: 'enum'; + options: Array; + defaultValue?: PluginArgumentValue; +}; + +export type PluginArgumentModel = PluginArgumentBase & { + type: 'model'; + model: string; + defaultValue?: string; +}; + +export type PluginArgumentString = PluginArgumentBase & { + type: 'string'; + placeholder?: string; + defaultValue?: string; +}; + +export type PluginArgumentBoolean = PluginArgumentBase & { + type: 'boolean'; + defaultValue?: boolean; +}; + +export type PluginArgumentFile = PluginArgumentBase & { + type: 'file'; +}; + +export type PluginArgumentNumber = PluginArgumentBase & { + type: 'number'; + placeholder?: string; + defaultValue?: number; +}; + +export type PluginArgument = + | PluginArgumentEnum + | PluginArgumentModel + | PluginArgumentString + | PluginArgumentBoolean + | PluginArgumentFile + | PluginArgumentNumber; + + export interface PluginTemplateTagContext { + util: { + models: { + request: { + getById: (id: string) => Promise; + }; + response: { + getLatestForRequestId: (id: string) => Promise; + getBodyBuffer: (response: Response, fallback?: any) => Promise; + }; + }; + }; +} + +export interface PluginTemplateTagActionContext { + store: PluginStore; +} + +export interface PluginTemplateTagAction { + name: string; + icon?: string; + run: (context: PluginTemplateTagActionContext) => Promise; +} + +export interface PluginTemplateTag { + args: Array; + name: string; + displayName: DisplayName; + disablePreview: () => boolean; + description: string; + actions: Array; + run: (context: PluginTemplateTagContext, ...arg: Array) => Promise | any; + deprecated?: boolean; + validate?: (value: any) => string | null; + priority?: number; +} diff --git a/packages/insomnia-app/app/templating/index.js b/packages/insomnia-app/app/templating/index.ts similarity index 83% rename from packages/insomnia-app/app/templating/index.js rename to packages/insomnia-app/app/templating/index.ts index 97f3a0277f..8bed91cd9e 100644 --- a/packages/insomnia-app/app/templating/index.js +++ b/packages/insomnia-app/app/templating/index.ts @@ -1,4 +1,3 @@ -// @flow import nunjucks from 'nunjucks'; import BaseExtension from './base-extension'; import type { NunjucksParsedTag } from './utils'; @@ -8,7 +7,11 @@ import type { TemplateTag } from '../plugins/index'; export class RenderError extends Error { message: string; path: string | null; - location: { line: number, column: number }; + location: { + line: number; + column: number; + }; + type: string; reason: string; } @@ -20,9 +23,9 @@ export const RENDER_TAGS = 'tags'; export const NUNJUCKS_TEMPLATE_GLOBAL_PROPERTY_NAME = '_'; // Cached globals -let nunjucksVariablesOnly = null; -let nunjucksTagsOnly = null; -let nunjucksAll = null; +let nunjucksVariablesOnly: nunjucks.Environment | null = null; +let nunjucksTagsOnly: nunjucks.Environment | null = null; +let nunjucksAll: nunjucks.Environment | null = null; /** * Render text based on stuff @@ -34,8 +37,12 @@ let nunjucksAll = null; */ export function render( text: string, - config: { context?: Object, path?: string, renderMode?: string } = {}, -): Promise { + config: { + context?: Record; + path?: string; + renderMode?: string; + } = {}, +) { const context = config.context || {}; // context needs to exist on the root for the old templating syntax, and in _ for the new templating syntax // old: {{ arr[0].prop }} @@ -43,28 +50,28 @@ export function render( const templatingContext = { ...context, [NUNJUCKS_TEMPLATE_GLOBAL_PROPERTY_NAME]: context }; const path = config.path || null; const renderMode = config.renderMode || RENDER_ALL; - - return new Promise(async (resolve, reject) => { + return new Promise(async (resolve, reject) => { const nj = await getNunjucks(renderMode); - nj.renderString(text, templatingContext, (err, result) => { + nj?.renderString(text, templatingContext, (err, result) => { if (err) { const sanitizedMsg = err.message .replace(/\(unknown path\)\s/, '') .replace(/\[Line \d+, Column \d*]/, '') .replace(/^\s*Error:\s*/, '') .trim(); - const location = err.message.match(/\[Line (\d)+, Column (\d)*]/); const line = location ? parseInt(location[1]) : 1; const column = location ? parseInt(location[2]) : 1; const reason = err.message.includes('attempted to output null or undefined value') ? 'undefined' : 'error'; - const newError = new RenderError(sanitizedMsg); newError.path = path || ''; newError.message = sanitizedMsg; - newError.location = { line, column }; + newError.location = { + line, + column, + }; newError.type = 'render'; newError.reason = reason; reject(newError); @@ -78,7 +85,7 @@ export function render( /** * Reload Nunjucks environments. Useful for if plugins change. */ -export function reload(): void { +export function reload() { nunjucksAll = null; nunjucksVariablesOnly = null; nunjucksTagsOnly = null; @@ -87,14 +94,15 @@ export function reload(): void { /** * Get definitions of template tags */ -export async function getTagDefinitions(): Promise> { +export async function getTagDefinitions() { const env = await getNunjucks(RENDER_ALL); - + // @ts-expect-error -- TSCONVERSION investigate why `extensions` isn't on Environment return Object.keys(env.extensions) + // @ts-expect-error -- TSCONVERSION investigate why `extensions` isn't on Environment .map(k => env.extensions[k]) .filter(ext => !ext.isDeprecated()) .sort((a, b) => (a.getPriority() > b.getPriority() ? 1 : -1)) - .map(ext => ({ + .map(ext => ({ name: ext.getTag(), displayName: ext.getName(), liveDisplayName: ext.getLiveDisplayName(), @@ -121,10 +129,11 @@ async function getNunjucks(renderMode: string) { // ~~~~~~~~~~~~ // // Setup Config // // ~~~~~~~~~~~~ // - const config = { - autoescape: false, // Don't escape HTML - throwOnUndefined: true, // Strict mode + autoescape: false, + // Don't escape HTML + throwOnUndefined: true, + // Strict mode tags: { blockStart: '{%', blockEnd: '%}', @@ -150,10 +159,9 @@ async function getNunjucks(renderMode: string) { // ~~~~~~~~~~~~~~~~~~~~~~~~~~ // // Create Env with Extensions // // ~~~~~~~~~~~~~~~~~~~~~~~~~~ // - const nj = nunjucks.configure(config); - let allTemplateTagPlugins: Array; + try { plugins.ignorePlugin('insomnia-plugin-kong-bundle'); allTemplateTagPlugins = await plugins.getTemplateTags(); @@ -162,12 +170,14 @@ async function getNunjucks(renderMode: string) { } const allExtensions = allTemplateTagPlugins; + for (let i = 0; i < allExtensions.length; i++) { const { templateTag, plugin } = allExtensions[i]; templateTag.priority = templateTag.priority || i * 100; + // @ts-expect-error -- TSCONVERSION const instance = new BaseExtension(templateTag, plugin); + // @ts-expect-error -- TSCONVERSION nj.addExtension(instance.getTag(), instance); - // Hidden helper filter to debug complicated things // eg. `{{ foo | urlencode | debug | upper }}` nj.addFilter('debug', o => o); @@ -176,7 +186,6 @@ async function getNunjucks(renderMode: string) { // ~~~~~~~~~~~~~~~~~~~~ // // Cache Env and Return // // ~~~~~~~~~~~~~~~~~~~~ // - if (renderMode === RENDER_VARS) { nunjucksVariablesOnly = nj; } else if (renderMode === RENDER_TAGS) { diff --git a/packages/insomnia-app/app/templating/utils.js b/packages/insomnia-app/app/templating/utils.ts similarity index 69% rename from packages/insomnia-app/app/templating/utils.js rename to packages/insomnia-app/app/templating/utils.ts index 1559d17535..0bf1315d32 100644 --- a/packages/insomnia-app/app/templating/utils.js +++ b/packages/insomnia-app/app/templating/utils.ts @@ -1,42 +1,45 @@ -// @flow - import type { PluginArgumentEnumOption } from './extensions'; import objectPath from 'objectpath'; import type { PluginStore } from '../plugins/context'; -export type NunjucksParsedTagArg = { - type: 'string' | 'number' | 'boolean' | 'variable' | 'expression' | 'enum' | 'file' | 'model', - encoding?: 'base64', - value: string | number | boolean, - defaultValue?: string | number | boolean, - forceVariable?: boolean, - placeholder?: string, - help?: string, - displayName?: string, - quotedBy?: '"' | "'", - validate?: (value: any) => string, - hide?: (Array) => boolean, - model?: string, - options?: Array, - itemTypes?: Array, - extensions?: Array, -}; +export interface NunjucksParsedTagArg { + type: 'string' | 'number' | 'boolean' | 'variable' | 'expression' | 'enum' | 'file' | 'model'; + encoding?: 'base64'; + value: string | number | boolean; + defaultValue?: string | number | boolean; + forceVariable?: boolean; + placeholder?: string; + help?: string; + displayName?: string; + quotedBy?: '"' | "'"; + validate?: (value: any) => string; + hide?: (arg0: Array) => boolean; + model?: string; + options?: Array; + itemTypes?: Array<'file' | 'directory'>; + extensions?: Array; +} -export type NunjucksActionTag = { - name: string, - icon?: string, - run: (context: PluginStore) => Promise, -}; +export interface NunjucksActionTag { + name: string; + icon?: string; + run: (context: PluginStore) => Promise; +} -export type NunjucksParsedTag = { - name: string, - args: Array, - actions: Array, - rawValue?: string, - displayName?: string, - description?: string, - disablePreview?: (Array) => boolean, -}; +export interface NunjucksParsedTag { + name: string; + args: Array; + actions?: Array; + rawValue?: string; + displayName?: string; + description?: string; + disablePreview?: (arg0: Array) => boolean; +} + +interface Key { + name: string; + value: any; +} /** * Get list of paths to all primitive types in nested object @@ -44,9 +47,11 @@ export type NunjucksParsedTag = { * @param {String} [prefix] - base path to prefix to all paths * @returns {Array} - list of paths */ -export function getKeys(obj: any, prefix: string = ''): Array<{ name: string, value: any }> { - let allKeys = []; - +export function getKeys( + obj: any, + prefix = '', +): Array { + let allKeys: Array = []; const typeOfObj = Object.prototype.toString.call(obj); if (typeOfObj === '[object Array]') { @@ -55,54 +60,50 @@ export function getKeys(obj: any, prefix: string = ''): Array<{ name: string, va } } else if (typeOfObj === '[object Object]') { const keys = Object.keys(obj); + for (const key of keys) { allKeys = [...allKeys, ...getKeys(obj[key], forceBracketNotation(prefix, key))]; } } else if (typeOfObj === '[object Function]') { // Ignore functions } else if (prefix) { - allKeys.push({ name: normalizeToDotAndBracketNotation(prefix), value: obj }); + allKeys.push({ + name: normalizeToDotAndBracketNotation(prefix), + value: obj, + }); } return allKeys; } -export function forceBracketNotation(prefix: string, key: string | number): string { +export function forceBracketNotation(prefix: string, key: string | number) { // Prefix is already in bracket notation because getKeys is recursive return `${prefix}${objectPath.stringify([key], "'", true)}`; } -export function normalizeToDotAndBracketNotation(prefix: string): string { +export function normalizeToDotAndBracketNotation(prefix: string) { return objectPath.normalize(prefix); } /** * Parse a Nunjucks tag string into a usable abject * @param {string} tagStr - the template string for the tag - * @return {object} parsed tag data */ -export function tokenizeTag(tagStr: string): NunjucksParsedTag { +export function tokenizeTag(tagStr: string) { // ~~~~~~~~ // // Sanitize // // ~~~~~~~~ // - - const withoutEnds = tagStr - .trim() - .replace(/^{%/, '') - .replace(/%}$/, '') - .trim(); - + const withoutEnds = tagStr.trim().replace(/^{%/, '').replace(/%}$/, '').trim(); const nameMatch = withoutEnds.match(/^[a-zA-Z_$][0-9a-zA-Z_$]*/); const name = nameMatch ? nameMatch[0] : withoutEnds; const argsStr = withoutEnds.slice(name.length); - // ~~~~~~~~~~~~~ // // Tokenize Args // // ~~~~~~~~~~~~~ // + const args: Array = []; + let quotedBy: string | null = null; + let currentArg: string | null = null; - const args = []; - let quotedBy = null; - let currentArg = null; for (let i = 0; i < argsStr.length + 1; i++) { // Adding an "invisible" at the end helps us terminate the last arg const c = argsStr.charAt(i) || ','; @@ -151,31 +152,52 @@ export function tokenizeTag(tagStr: string): NunjucksParsedTag { // End current argument if (currentArg !== null && argCompleted) { let arg; + if (quotedBy) { - arg = { type: 'string', value: currentArg, quotedBy }; + arg = { + type: 'string', + value: currentArg, + quotedBy, + }; } else if (['true', 'false'].includes(currentArg)) { - arg = { type: 'boolean', value: currentArg.toLowerCase() === 'true' }; + arg = { + type: 'boolean', + value: currentArg.toLowerCase() === 'true', + }; } else if (currentArg.match(/^\d*\.?\d*$/)) { - arg = { type: 'number', value: currentArg }; + arg = { + type: 'number', + value: currentArg, + }; } else if (currentArg.match(/^[a-zA-Z_$][0-9a-zA-Z_$]*$/)) { - arg = { type: 'variable', value: currentArg }; + arg = { + type: 'variable', + value: currentArg, + }; } else { - arg = { type: 'expression', value: currentArg }; + arg = { + type: 'expression', + value: currentArg, + }; } args.push(arg); - currentArg = null; quotedBy = null; } } - return { name, args }; + const parsedTag: NunjucksParsedTag = { + name, + args, + }; + return parsedTag; } /** Convert a tokenized tag back into a Nunjucks string */ -export function unTokenizeTag(tagData: NunjucksParsedTag): string { - const args = []; +export function unTokenizeTag(tagData: NunjucksParsedTag) { + const args: Array = []; + for (const arg of tagData.args) { if (['string', 'model', 'file', 'enum'].includes(arg.type)) { const q = arg.quotedBy || "'"; @@ -185,6 +207,7 @@ export function unTokenizeTag(tagData: NunjucksParsedTag): string { } else if (arg.type === 'boolean') { args.push(arg.value ? 'true' : 'false'); } else { + // @ts-expect-error -- TSCONVERSION args.push(arg.value); } } @@ -194,7 +217,7 @@ export function unTokenizeTag(tagData: NunjucksParsedTag): string { } /** Get the default Nunjucks string for an extension */ -export function getDefaultFill(name: string, args: Array): string { +export function getDefaultFill(name: string, args: Array) { const stringArgs: Array = (args || []).map(argDefinition => { switch (argDefinition.type) { case 'enum': @@ -202,23 +225,27 @@ export function getDefaultFill(name: string, args: Array): const fallback = options && options.length ? options[0].value : ''; const value = defaultValue !== undefined ? String(defaultValue) : String(fallback); return `'${value}'`; + case 'number': + // @ts-expect-error -- TSCONVERSION return `${parseFloat(argDefinition.defaultValue) || 0}`; + case 'boolean': return argDefinition.defaultValue ? 'true' : 'false'; + case 'string': case 'file': case 'model': - return `'${(argDefinition.defaultValue: any) || ''}'`; + return `'${(argDefinition.defaultValue as any) || ''}'`; + default: return "''"; } }); - return `${name} ${stringArgs.join(', ')}`; } -export function encodeEncoding(value: string, encoding: 'base64'): string { +export function encodeEncoding(value: string, encoding: 'base64') { if (typeof value !== 'string') { return value; } @@ -231,12 +258,13 @@ export function encodeEncoding(value: string, encoding: 'base64'): string { return value; } -export function decodeEncoding(value: string): string { +export function decodeEncoding(value: string) { if (typeof value !== 'string') { return value; } const results = value.match(/^b64::(.+)::46b$/); + if (results) { return Buffer.from(results[1], 'base64').toString('utf8'); } diff --git a/packages/insomnia-app/app/test-utils.js b/packages/insomnia-app/app/test-utils.ts similarity index 68% rename from packages/insomnia-app/app/test-utils.js rename to packages/insomnia-app/app/test-utils.ts index 6797ec3fb1..112546c56d 100644 --- a/packages/insomnia-app/app/test-utils.js +++ b/packages/insomnia-app/app/test-utils.ts @@ -1,36 +1,41 @@ -// @flow - import * as modals from './ui/components/modals'; import type { ErrorModalOptions } from './ui/components/modals/error-modal'; export const getAndClearShowPromptMockArgs = () => { - const mockFn = (modals.showPrompt: JestMockFn); + const mockFn = modals.showPrompt as jest.Mock; const { title, submitName, defaultValue, onComplete } = mockFn.mock.calls[0][0]; mockFn.mockClear(); - - return { title, submitName, defaultValue, onComplete }; + return { + title, + submitName, + defaultValue, + onComplete, + }; }; export const getAndClearShowAlertMockArgs = () => { - const mockFn = (modals.showAlert: JestMockFn); + const mockFn = modals.showAlert as jest.Mock; const { title, okLabel, addCancel, message, onConfirm } = mockFn.mock.calls[0][0]; mockFn.mockClear(); - - return { title, okLabel, addCancel, message, onConfirm }; + return { + title, + okLabel, + addCancel, + message, + onConfirm, + }; }; export const getAndClearShowErrorMockArgs = (): ErrorModalOptions => { - const mockFn = (modals.showError: JestMockFn); + const mockFn = modals.showError as jest.Mock; const options: ErrorModalOptions = mockFn.mock.calls[0][0]; mockFn.mockClear(); - return options; }; export const getAndClearShowModalMockArgs = () => { - const mockFn = (modals.showModal: JestMockFn); + const mockFn = modals.showModal as jest.Mock; const args = mockFn.mock.calls[0][1]; mockFn.mockClear(); - return args; }; diff --git a/packages/insomnia-app/app/ui/components/activity-toggle.js b/packages/insomnia-app/app/ui/components/activity-toggle.tsx similarity index 51% rename from packages/insomnia-app/app/ui/components/activity-toggle.js rename to packages/insomnia-app/app/ui/components/activity-toggle.tsx index 2c7531e4dd..10118b1115 100644 --- a/packages/insomnia-app/app/ui/components/activity-toggle.js +++ b/packages/insomnia-app/app/ui/components/activity-toggle.tsx @@ -1,30 +1,39 @@ -// @flow - -import React from 'react'; +import React, { FunctionComponent } from 'react'; import { MultiSwitch } from 'insomnia-components'; import type { GlobalActivity } from '../../common/constants'; import { ACTIVITY_DEBUG, ACTIVITY_SPEC, ACTIVITY_UNIT_TEST } from '../../common/constants'; import type { Workspace } from '../../models/workspace'; -type Props = { - activity: GlobalActivity, - handleActivityChange: (workspaceId: string, activity: GlobalActivity) => Promise, - workspace: Workspace, -}; +interface Props { + activity: GlobalActivity; + handleActivityChange: (workspaceId: string, activity: GlobalActivity) => Promise; + workspace: Workspace; +} -export default function ActivityToggle({ activity, handleActivityChange, workspace }: Props) { +const ActivityToggle: FunctionComponent = ({ activity, handleActivityChange, workspace }) => { const choices = [ - { label: 'Design', value: ACTIVITY_SPEC }, - { label: 'Debug', value: ACTIVITY_DEBUG }, - { label: 'Test', value: ACTIVITY_UNIT_TEST }, + { + label: 'Design', + value: ACTIVITY_SPEC, + }, + { + label: 'Debug', + value: ACTIVITY_DEBUG, + }, + { + label: 'Test', + value: ACTIVITY_UNIT_TEST, + }, ]; - return ( handleActivityChange(workspace._id, a)} choices={choices} selectedValue={activity} /> ); -} +}; + +export default ActivityToggle; diff --git a/packages/insomnia-app/app/ui/components/analytics.js b/packages/insomnia-app/app/ui/components/analytics.tsx similarity index 76% rename from packages/insomnia-app/app/ui/components/analytics.js rename to packages/insomnia-app/app/ui/components/analytics.tsx index 5b74850105..9b534653aa 100644 --- a/packages/insomnia-app/app/ui/components/analytics.js +++ b/packages/insomnia-app/app/ui/components/analytics.tsx @@ -1,5 +1,4 @@ -// @flow -import * as React from 'react'; +import React, { Fragment, PureComponent } from 'react'; import { autoBindMethodsForReact } from 'class-autobind-decorator'; import { Button } from 'insomnia-components'; import * as models from '../../models'; @@ -7,33 +6,33 @@ import { AUTOBIND_CFG, getAppLongName } from '../../common/constants'; import type { WrapperProps } from './wrapper'; import chartSrc from '../images/chart.svg'; -type Props = { - wrapperProps: WrapperProps, - handleDone: Function, -}; +interface Props { + wrapperProps: WrapperProps; + handleDone: (...args: Array) => any; +} @autoBindMethodsForReact(AUTOBIND_CFG) -class Analytics extends React.PureComponent { +class Analytics extends PureComponent { async _handleAnalyticsSetting(enableAnalytics: boolean) { const { settings } = this.props.wrapperProps; - // Update settings with analytics preferences - await models.settings.update(settings, { enableAnalytics }); - + await models.settings.update(settings, { + enableAnalytics, + }); this.props.handleDone(); } - async _handleClickEnableAnalytics(e: SyntheticEvent) { + async _handleClickEnableAnalytics() { this._handleAnalyticsSetting(true); } - async _handleClickDisableAnalytics(e: SyntheticEvent) { + async _handleClickDisableAnalytics() { this._handleAnalyticsSetting(false); } render() { return ( - +

Share Usage Analytics with Kong Inc

@@ -57,7 +56,7 @@ class Analytics extends React.PureComponent { onClick={this._handleClickDisableAnalytics}> Don't share usage analytics -
+ ); } } diff --git a/packages/insomnia-app/app/ui/components/base/__tests__/editable.test.js b/packages/insomnia-app/app/ui/components/base/__tests__/editable.test.ts similarity index 100% rename from packages/insomnia-app/app/ui/components/base/__tests__/editable.test.js rename to packages/insomnia-app/app/ui/components/base/__tests__/editable.test.ts diff --git a/packages/insomnia-app/app/ui/components/base/button.js b/packages/insomnia-app/app/ui/components/base/button.tsx similarity index 51% rename from packages/insomnia-app/app/ui/components/base/button.js rename to packages/insomnia-app/app/ui/components/base/button.tsx index b276352781..4dafe81aa6 100644 --- a/packages/insomnia-app/app/ui/components/base/button.js +++ b/packages/insomnia-app/app/ui/components/base/button.tsx @@ -1,11 +1,24 @@ -import React, { PureComponent } from 'react'; -import PropTypes from 'prop-types'; +import React, { ButtonHTMLAttributes, PureComponent, ReactNode } from 'react'; import { autoBindMethodsForReact } from 'class-autobind-decorator'; import { AUTOBIND_CFG } from '../../../common/constants'; +interface Props { + children: ReactNode, + value?: any, + className?: string, + onDisabledClick?: Function, + onClick?: React.MouseEventHandler | ((value: any, e: React.MouseEvent) => void), + disabled?: boolean, + tabIndex?: number, + type?: ButtonHTMLAttributes['type'], + id?: string, + // TODO(TSCONVERSION) figure out why so many components pass this yet it isn't used + title?: string; +} + @autoBindMethodsForReact(AUTOBIND_CFG) -class Button extends PureComponent { - _handleClick(e) { +class Button extends PureComponent { + _handleClick(e: React.MouseEvent) { const { onClick, onDisabledClick, disabled } = this.props; const fn = disabled ? onDisabledClick : onClick; @@ -18,7 +31,6 @@ class Button extends PureComponent { render() { const { children, disabled, tabIndex, className, type, id } = this.props; - return ( ); } } -Button.propTypes = { - // Required - children: PropTypes.node.isRequired, - - // Optional - value: PropTypes.any, - className: PropTypes.string, - onDisabledClick: PropTypes.func, - onClick: PropTypes.func, - disabled: PropTypes.bool, - tabIndex: PropTypes.number, - type: PropTypes.string, - id: PropTypes.string, -}; - export default Button; diff --git a/packages/insomnia-app/app/ui/components/base/copy-button.js b/packages/insomnia-app/app/ui/components/base/copy-button.tsx similarity index 63% rename from packages/insomnia-app/app/ui/components/base/copy-button.js rename to packages/insomnia-app/app/ui/components/base/copy-button.tsx index ad2ae348ac..f5c58e3b94 100644 --- a/packages/insomnia-app/app/ui/components/base/copy-button.js +++ b/packages/insomnia-app/app/ui/components/base/copy-button.tsx @@ -1,23 +1,31 @@ -import React, { PureComponent } from 'react'; +import React, { PureComponent, ReactNode } from 'react'; import { clipboard } from 'electron'; -import PropTypes from 'prop-types'; import { autoBindMethodsForReact } from 'class-autobind-decorator'; import { AUTOBIND_CFG } from '../../../common/constants'; -import { Button } from 'insomnia-components'; +import { Button, ButtonProps } from 'insomnia-components'; + +interface Props extends ButtonProps { + content: string | Function, + children?: ReactNode, + title?: string, + confirmMessage?: string, +} + +interface State { + showConfirmation: boolean; +} @autoBindMethodsForReact(AUTOBIND_CFG) -class CopyButton extends PureComponent { - constructor(props) { - super(props); - this.state = { - showConfirmation: false, - }; +class CopyButton extends PureComponent { + state: State = { + showConfirmation: false, } + _triggerTimeout: NodeJS.Timeout | null = null; + async _handleClick(e) { e.preventDefault(); e.stopPropagation(); - const content = typeof this.props.content === 'string' ? this.props.content : await this.props.content(); @@ -25,29 +33,33 @@ class CopyButton extends PureComponent { clipboard.writeText(content); } - this.setState({ showConfirmation: true }); - + this.setState({ + showConfirmation: true, + }); this._triggerTimeout = setTimeout(() => { - this.setState({ showConfirmation: false }); + this.setState({ + showConfirmation: false, + }); }, 2000); } componentWillUnmount() { + if (this._triggerTimeout === null) { + return; + } clearTimeout(this._triggerTimeout); } render() { const { - content, // eslint-disable-line no-unused-vars + content, children, title, confirmMessage, ...other } = this.props; const { showConfirmation } = this.state; - const confirm = typeof confirmMessage === 'string' ? confirmMessage : 'Copied'; - return (