diff --git a/__mocks__/obsidian/Modal.ts b/__mocks__/obsidian/Modal.ts index cf6e54b..efac4ae 100644 --- a/__mocks__/obsidian/Modal.ts +++ b/__mocks__/obsidian/Modal.ts @@ -1,5 +1,4 @@ -import type { App, Scope } from "obsidian"; -import type { Modal } from "obsidian"; +import type { App, Scope, Modal } from "obsidian"; export class ModalMock implements Modal { constructor(public app: App) {} diff --git a/eslint.config.mjs b/eslint.config.mjs index d9d43a9..7ca99da 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -5,6 +5,7 @@ import tseslint from "typescript-eslint"; import UnusedImportsPlugin from "eslint-plugin-unused-imports"; import PrettierConfig from "eslint-config-prettier"; import JestPlugin from "eslint-plugin-jest"; +import * as ImportPlugin from "eslint-plugin-import"; export default tseslint.config( { @@ -23,6 +24,7 @@ export default tseslint.config( plugins: { "unused-imports": UnusedImportsPlugin, jest: JestPlugin, + import: ImportPlugin, }, rules: { "@typescript-eslint/restrict-template-expressions": "warn", @@ -47,6 +49,8 @@ export default tseslint.config( argsIgnorePattern: "^_", }, ], + "import/order": "error", + "import/no-duplicates": "error", }, }, PrettierConfig, diff --git a/main.ts b/main.ts index bdf64a7..34c7011 100644 --- a/main.ts +++ b/main.ts @@ -1,2 +1,2 @@ -import { GithubLinkPlugin } from "src/plugin"; +import { GithubLinkPlugin } from "./src/plugin"; export default GithubLinkPlugin; diff --git a/package-lock.json b/package-lock.json index b1f40de..eca8e80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,17 +14,20 @@ "@octokit/openapi-types": "^19.1.0", "@octokit/plugin-rest-endpoint-methods": "^10.3.0", "@octokit/request": "^8.2.0", + "lodash": "^4.17.21", "queue": "^7.0.0" }, "devDependencies": { "@codemirror/view": "^6.23.0", "@eslint/js": "^9.1.1", "@jest/globals": "^29.7.0", + "@types/lodash": "^4.17.5", "@types/node": "^20.11.19", "builtin-modules": "3.3.0", "esbuild": "0.20.0", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", + "eslint-plugin-import": "^2.29.1", "eslint-plugin-jest": "^28.2.0", "eslint-plugin-unused-imports": "^3.1.0", "jest": "^29.7.0", @@ -2137,6 +2140,12 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, "node_modules/@types/jsonwebtoken": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.5.tgz", @@ -2146,6 +2155,12 @@ "@types/node": "*" } }, + "node_modules/@types/lodash": { + "version": "4.17.5", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.5.tgz", + "integrity": "sha512-MBIOHVZqVqgfro1euRDWX7OO0fBVUUMrN6Pwm8LQsz8cWhEpihlvR70ENj3f40j58TNxZaWv2ndSkInykNBBJw==", + "dev": true + }, "node_modules/@types/node": { "version": "20.11.19", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz", @@ -2551,6 +2566,42 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/array-buffer-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz", + "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array-includes": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", + "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.4", + "is-string": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -2560,12 +2611,105 @@ "node": ">=8" } }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz", + "integrity": "sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flat": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", + "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/array.prototype.flatmap": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", + "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", + "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "es-abstract": "^1.22.3", + "es-errors": "^1.2.1", + "get-intrinsic": "^1.2.3", + "is-array-buffer": "^3.0.4", + "is-shared-array-buffer": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "dev": true }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -2802,6 +2946,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -3040,6 +3203,57 @@ "node": ">=12" } }, + "node_modules/data-view-buffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.1.tgz", + "integrity": "sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz", + "integrity": "sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-offset": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", + "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -3092,6 +3306,40 @@ "node": ">=0.10.0" } }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -3215,6 +3463,139 @@ "is-arrayish": "^0.2.1" } }, + "node_modules/es-abstract": { + "version": "1.23.3", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", + "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "dev": true, + "dependencies": { + "array-buffer-byte-length": "^1.0.1", + "arraybuffer.prototype.slice": "^1.0.3", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "data-view-buffer": "^1.0.1", + "data-view-byte-length": "^1.0.1", + "data-view-byte-offset": "^1.0.0", + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-set-tostringtag": "^2.0.3", + "es-to-primitive": "^1.2.1", + "function.prototype.name": "^1.1.6", + "get-intrinsic": "^1.2.4", + "get-symbol-description": "^1.0.2", + "globalthis": "^1.0.3", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.0.3", + "has-symbols": "^1.0.3", + "hasown": "^2.0.2", + "internal-slot": "^1.0.7", + "is-array-buffer": "^3.0.4", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.1", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.3", + "is-string": "^1.0.7", + "is-typed-array": "^1.1.13", + "is-weakref": "^1.0.2", + "object-inspect": "^1.13.1", + "object-keys": "^1.1.1", + "object.assign": "^4.1.5", + "regexp.prototype.flags": "^1.5.2", + "safe-array-concat": "^1.1.2", + "safe-regex-test": "^1.0.3", + "string.prototype.trim": "^1.2.9", + "string.prototype.trimend": "^1.0.8", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.2", + "typed-array-byte-length": "^1.0.1", + "typed-array-byte-offset": "^1.0.2", + "typed-array-length": "^1.0.6", + "unbox-primitive": "^1.0.2", + "which-typed-array": "^1.1.15" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", + "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz", + "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz", + "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==", + "dev": true, + "dependencies": { + "hasown": "^2.0.0" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/esbuild": { "version": "0.20.0", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.0.tgz", @@ -3362,6 +3743,135 @@ "eslint": ">=7.0.0" } }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz", + "integrity": "sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==", + "dev": true, + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", + "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.7", + "array.prototype.findlastindex": "^1.2.3", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.8.0", + "hasown": "^2.0.0", + "is-core-module": "^2.13.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.7", + "object.groupby": "^1.0.1", + "object.values": "^1.1.7", + "semver": "^6.3.1", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/eslint-plugin-jest": { "version": "28.2.0", "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.2.0.tgz", @@ -3835,6 +4345,15 @@ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.3" + } + }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -3878,6 +4397,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/function.prototype.name": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", + "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "functions-have-names": "^1.2.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -3896,6 +4442,25 @@ "node": "6.* || 8.* || >= 10.*" } }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -3917,6 +4482,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-symbol-description": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", + "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -3986,6 +4568,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/globby": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", @@ -4006,6 +4604,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -4018,6 +4628,15 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -4027,6 +4646,57 @@ "node": ">=8" } }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -4183,12 +4853,82 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "node_modules/internal-slot": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", + "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.0", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", + "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", "dev": true }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-core-module": { "version": "2.13.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", @@ -4201,6 +4941,36 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-data-view": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", + "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "dev": true, + "dependencies": { + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -4240,6 +5010,18 @@ "node": ">=0.10.0" } }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -4249,6 +5031,21 @@ "node": ">=0.12.0" } }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-path-inside": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", @@ -4264,6 +5061,37 @@ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", "dev": true }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", + "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -4276,6 +5104,69 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "dev": true, + "dependencies": { + "which-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -5145,6 +6036,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -5314,6 +6210,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -5365,6 +6270,94 @@ "integrity": "sha512-2f3F0SEEer8bBu0dsNCFF50N0cTThV1nWFYcEYFZttdW0lDAoybv9cQoK7X7/68Z89S7FoRrVjP1LPX4XRf9vg==", "dev": true }, + "node_modules/object-inspect": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", + "integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.values": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", + "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/obsidian": { "version": "1.4.11", "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.4.11.tgz", @@ -5663,6 +6656,15 @@ "node": ">=8" } }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5794,6 +6796,24 @@ "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", "dev": true }, + "node_modules/regexp.prototype.flags": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", + "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "set-function-name": "^2.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -5913,6 +6933,24 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/safe-array-concat": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", + "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "get-intrinsic": "^1.2.4", + "has-symbols": "^1.0.3", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -5933,6 +6971,23 @@ } ] }, + "node_modules/safe-regex-test": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", + "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.6", + "es-errors": "^1.3.0", + "is-regex": "^1.1.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -5966,6 +7021,38 @@ "node": ">=10" } }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -5987,6 +7074,24 @@ "node": ">=8" } }, + "node_modules/side-channel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", + "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.4", + "object-inspect": "^1.13.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", @@ -6081,6 +7186,55 @@ "node": ">=8" } }, + "node_modules/string.prototype.trim": { + "version": "1.2.9", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", + "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", + "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -6310,6 +7464,39 @@ } } }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", @@ -6349,6 +7536,79 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/typed-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", + "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/typed-array-byte-length": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", + "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", + "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", + "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-proto": "^1.0.3", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typescript": { "version": "5.4.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", @@ -6388,6 +7648,21 @@ } } }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -6566,6 +7841,41 @@ "node": ">= 8" } }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/package.json b/package.json index 4e6c270..dd07e1e 100644 --- a/package.json +++ b/package.json @@ -20,11 +20,13 @@ "@codemirror/view": "^6.23.0", "@eslint/js": "^9.1.1", "@jest/globals": "^29.7.0", + "@types/lodash": "^4.17.5", "@types/node": "^20.11.19", "builtin-modules": "3.3.0", "esbuild": "0.20.0", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", + "eslint-plugin-import": "^2.29.1", "eslint-plugin-jest": "^28.2.0", "eslint-plugin-unused-imports": "^3.1.0", "jest": "^29.7.0", @@ -43,6 +45,7 @@ "@octokit/openapi-types": "^19.1.0", "@octokit/plugin-rest-endpoint-methods": "^10.3.0", "@octokit/request": "^8.2.0", + "lodash": "^4.17.21", "queue": "^7.0.0" } -} \ No newline at end of file +} diff --git a/src/github/api.ts b/src/github/api.ts index 2c0888c..bcfd00f 100644 --- a/src/github/api.ts +++ b/src/github/api.ts @@ -1,3 +1,9 @@ +import type { RequestUrlParam, RequestUrlResponse } from "obsidian"; + +import { Notice, requestUrl } from "obsidian"; +import Queue from "queue"; +import { PluginSettings, getCache, logger } from "../plugin"; +import { RequestError, isSuccessResponse, promiseWithResolvers } from "../util"; import type { CheckRunListResponse, CodeResponse, @@ -6,16 +12,13 @@ import type { IssueResponse, IssueSearchParams, IssueSearchResponse, + LinkMeta, + MaybePaginated, + PaginationMeta, PullListParams, PullListResponse, PullResponse, } from "./response"; -import type { RequestUrlParam, RequestUrlResponse } from "obsidian"; - -import { RequestError, isSuccessResponse, promiseWithResolvers } from "src/util"; -import { Notice, requestUrl } from "obsidian"; -import { PluginSettings, getCache, logger } from "src/plugin"; -import Queue from "queue"; import type { CacheEntry } from "./cache"; type RequestConfig = RequestUrlParam & { headers: Record }; @@ -25,17 +28,21 @@ export class GitHubApi { private static rateLimitReset: Date | null = null; private static q = new Queue({ autostart: true, concurrency: 1 }); - public queueRequest(config: RequestUrlParam, token?: string): Promise { + public queueRequest( + config: RequestUrlParam, + token?: string, + skipCache = false, + ): Promise> { // Responses we (probably) have cached will skip the queue if (getCache().get(config)) { - return this.githubRequest(config, token); + return this.githubRequest(config, token, skipCache); } - const { resolve, reject, promise } = promiseWithResolvers(); + const { resolve, reject, promise } = promiseWithResolvers>(); GitHubApi.q.push(() => { - return this.githubRequest(config, token) - .then((result) => { - resolve(result); + return this.githubRequest(config, token, skipCache) + .then((response) => { + resolve(response); }) .catch((err) => { reject(err); @@ -44,16 +51,65 @@ export class GitHubApi { return promise; } - public async getIssue(org: string, repo: string, issue: number, token?: string): Promise { - const result = await this.queueRequest({ url: `${GitHubApi.baseApi}/repos/${org}/${repo}/issues/${issue}` }, token); - - return result.json as IssueResponse; + public getPaginationMeta(response: RequestUrlResponse): MaybePaginated { + const meta = this.parseLinkHeader(response.headers["link"]); + return { meta, response }; } - public async listIssuesForToken(params: IssueListParams, token: string): Promise { + public parseLinkHeader(link: string | undefined): PaginationMeta { + logger.debug(`Parsing link header: ${link}`); + const paginationMeta: PaginationMeta = {}; + if (!link) { + return paginationMeta; + } + const linkRelPattern = /<(?http[^\s?]+)\?(?[^\s>]*)>;\s*rel="(?[^\s"]*)"/g; + let match: RegExpExecArray | null = null; + do { + match = linkRelPattern.exec(link); + logger.debug(match); + if (match?.groups) { + const params = new URLSearchParams(match.groups.qp); + const page = parseInt(params.get("page") ?? ""); + const per_page = parseInt(params.get("per_page") ?? ""); + if (isNaN(page) || isNaN(per_page)) { + continue; + } + const linkMeta: LinkMeta = { + url: match.groups.url, + page, + per_page, + }; + paginationMeta[match.groups.rel as keyof PaginationMeta] = linkMeta; + } + } while (match); + logger.debug(paginationMeta); + return paginationMeta; + } + + public async getIssue( + org: string, + repo: string, + issue: number, + token?: string, + skipCache = false, + ): Promise { + const { response } = await this.queueRequest( + { url: `${GitHubApi.baseApi}/repos/${org}/${repo}/issues/${issue}` }, + token, + skipCache, + ); + + return response.json as IssueResponse; + } + + public async listIssuesForToken( + params: IssueListParams, + token: string, + skipCache = false, + ): Promise> { const url = this.addParams(`${GitHubApi.baseApi}/issues`, params as Record); - const result = await this.queueRequest({ url }, token); - return result.json as IssueListResponse; + const { meta, response } = await this.queueRequest({ url }, token, skipCache); + return { meta, response: response.json as IssueListResponse }; } public async listIssuesForRepo( @@ -61,20 +117,28 @@ export class GitHubApi { repo: string, params: IssueListParams = {}, token?: string, - ): Promise { + skipCache = false, + ): Promise> { const url = this.addParams(`${GitHubApi.baseApi}/repos/${org}/${repo}/issues`, params as Record); - const result = await this.queueRequest({ url }, token); - return result.json as IssueListResponse; + const { meta, response } = await this.queueRequest({ url }, token, skipCache); + return { meta, response: response.json as IssueListResponse }; } - public async getPullRequest(org: string, repo: string, pr: number, token?: string): Promise { - const result = await this.queueRequest( + public async getPullRequest( + org: string, + repo: string, + pr: number, + token?: string, + skipCache = false, + ): Promise { + const { response } = await this.queueRequest( { url: `${GitHubApi.baseApi}/repos/${org}/${repo}/pulls/${pr}`, }, token, + skipCache, ); - return result.json as PullResponse; + return response.json as PullResponse; } public async listPullRequestsForRepo( @@ -82,26 +146,39 @@ export class GitHubApi { repo: string, params: PullListParams = {}, token?: string, - ): Promise { + skipCache = false, + ): Promise> { const url = this.addParams(`${GitHubApi.baseApi}/repos/${org}/${repo}/pulls`, params as Record); - const result = await this.queueRequest({ url }, token); - return result.json as PullListResponse; + const { meta, response } = await this.queueRequest({ url }, token, skipCache); + return { meta, response: response.json as PullListResponse }; } - public async getCode(org: string, repo: string, path: string, branch: string, token?: string): Promise { - const result = await this.queueRequest( + public async getCode( + org: string, + repo: string, + path: string, + branch: string, + token?: string, + skipCache = false, + ): Promise { + const { response } = await this.queueRequest( { url: `${GitHubApi.baseApi}/repos/${org}/${repo}/contents/${path}?ref=${branch}`, }, token, + skipCache, ); - return result.json as CodeResponse; + return response.json as CodeResponse; } - public async searchIssues(params: IssueSearchParams, token?: string): Promise { + public async searchIssues( + params: IssueSearchParams, + token?: string, + skipCache = false, + ): Promise> { const url = this.addParams(`${GitHubApi.baseApi}/search/issues`, params); - const result = await this.githubRequest({ url }, token); - return result.json as IssueSearchResponse; + const { meta, response } = await this.queueRequest({ url }, token, skipCache); + return { meta, response: response.json as IssueSearchResponse }; } public async listCheckRunsForRef( @@ -109,19 +186,21 @@ export class GitHubApi { repo: string, ref: string, token?: string, + skipCache = false, ): Promise { - const result = await this.githubRequest( + const { response } = await this.githubRequest( { url: `${GitHubApi.baseApi}/${org}/${repo}/commits/${ref}/check-runs` }, token, + skipCache, ); - return result.json as CheckRunListResponse; + return response.json as CheckRunListResponse; } private async githubRequest( _config: RequestUrlParam, token?: string, skipCache = false, - ): Promise { + ): Promise> { if (GitHubApi.rateLimitReset !== null && GitHubApi.rateLimitReset > new Date()) { logger.warn( `GitHub rate limit exceeded. No more requests will be made until ${GitHubApi.rateLimitReset.toLocaleTimeString()}`, @@ -133,17 +212,20 @@ export class GitHubApi { } const config = this.initHeaders(_config, token); + let cachedValue: CacheEntry | null = null; - // Check request cache first - const cachedValue = getCache().get(config); - if (this.cachedRequestIsRecent(cachedValue, skipCache)) { - logger.debug(`Request was too recent. Returning cached value for: ${cachedValue?.request.url}`); - logger.debug(cachedValue?.response); - return cachedValue!.response; + if (!skipCache) { + // Check request cache first + cachedValue = getCache().get(config); + if (this.cachedRequestIsRecent(cachedValue)) { + logger.debug(`Request was too recent. Returning cached value for: ${cachedValue?.request.url}`); + logger.debug(cachedValue?.response); + return this.getPaginationMeta(cachedValue!.response); + } + + this.setCacheHeaders(config, cachedValue); } - this.setCacheHeaders(config, cachedValue); - try { logger.debug(`Request: ${config.url}`); logger.debug(config); @@ -154,7 +236,7 @@ export class GitHubApi { // Check for 304 response, return cached value if (cachedValue?.response && response.status === 304) { getCache().update(config); - return cachedValue.response; + return this.getPaginationMeta(cachedValue.response); } else if (isSuccessResponse(response.status)) { getCache().set(config, response); } @@ -177,7 +259,7 @@ export class GitHubApi { logger.warn("GitHub rate limit approaching."); } - return response; + return this.getPaginationMeta(response); } catch (err) { logger.debug(err); return Promise.reject(new RequestError(err as Error)); @@ -223,8 +305,8 @@ export class GitHubApi { /** * Returns true if we can skip calling the API due to request age */ - private cachedRequestIsRecent(cachedValue: CacheEntry | null, skipCache: boolean): boolean { - if (skipCache || !cachedValue) { + private cachedRequestIsRecent(cachedValue: CacheEntry | null): boolean { + if (!cachedValue) { return false; } // Return the cached value if it was recent enough diff --git a/src/github/cache.ts b/src/github/cache.ts index 58d46a7..fb8c2b9 100644 --- a/src/github/cache.ts +++ b/src/github/cache.ts @@ -1,16 +1,6 @@ import type { RequestUrlParam, RequestUrlResponse } from "obsidian"; -import type { - IssueListParams, - IssueListResponse, - IssueResponse, - IssueSearchResponse, - PullListParams, - PullListResponse, - PullResponse, - RepoSearchResponse, -} from "./response"; -import { logger } from "src/plugin"; -import { isSuccessResponse } from "src/util"; +import { logger } from "../plugin"; +import { isSuccessResponse, sanitizeObject } from "../util"; interface CacheParams { request: RequestUrlParam; @@ -84,7 +74,12 @@ export class RequestCache { } public get(request: RequestUrlParam): CacheEntry | null { - return this.entries[this.getCacheKey(request)] ?? null; + const entry: CacheEntry | null = this.entries[this.getCacheKey(request)] ?? null; + // Ensure headers are defined; some old cache entries might not have them + if (entry && !entry.response.headers) { + entry.response.headers = {}; + } + return entry; } public set(request: RequestUrlParam, response: RequestUrlResponse): void { @@ -99,7 +94,11 @@ export class RequestCache { // Slim down the data we store const _request: Partial = { url: request.url, body: request.body }; - const _response: Partial = { json: response.json, status: response.status }; + const _response: Partial = { + json: response.json, + status: response.status, + headers: sanitizeObject(response.headers, { link: true }), + }; const entry = new CacheEntry( _request as RequestUrlParam, @@ -153,155 +152,3 @@ export class RequestCache { return request.url; } } - -class OldCacheEntry { - constructor( - public value: T, - public created: Date = new Date(), - public ttl: number = 20, - ) {} - - get expired(): boolean { - const expiry = this.created.getTime() + this.ttl * 60 * 1000; - return new Date().getTime() > expiry; - } -} - -class OldQueryCache { - public readonly issueCache: Record> = {}; - public readonly repoCache: Record> = {}; -} - -class OldRepoCache { - public readonly issueCache: Record> = {}; - public readonly issueListForRepoCache: Record> = {}; - public readonly pullCache: Record> = {}; - public readonly pullListForRepoCache: Record> = {}; -} - -class OldOrgCache { - public readonly repos: Record = {}; - public readonly issueList: Record> = {}; -} - -/** - * @deprecated Should remove this in the one place its still used, but need an alternative solution first - */ -export class OldCache { - public readonly generic: Record> = {}; - public readonly orgs: Record = {}; - public readonly queries = new OldQueryCache(); - - getGeneric(url: string): unknown { - return this.getCacheValue(this.generic[url] ?? null); - } - - setGeneric(url: string, value: unknown): void { - this.generic[url] = new OldCacheEntry(value); - } - - getIssue(org: string, repo: string, issue: number): IssueResponse | null { - const repoCache = this.getRepoCache(org, repo); - return this.getCacheValue(repoCache.issueCache[issue] ?? null); - } - - setIssue(org: string, repo: string, issue: IssueResponse): void { - const issueCache = this.getRepoCache(org, repo).issueCache; - const existingCache = issueCache[issue.number]; - if (existingCache) { - const now = new Date(); - existingCache.created = now; - existingCache.value = issue; - } else { - issueCache[issue.number] = new OldCacheEntry(issue); - } - } - - getIssueList(org: string, params: IssueListParams): IssueListResponse | null { - const orgCache = this.getOrgCache(org); - return this.getCacheValue(orgCache.issueList[JSON.stringify(params)] ?? null); - } - - setIssueList(org: string, params: IssueListParams, value: IssueListResponse): void { - const orgCache = this.getOrgCache(org); - orgCache.issueList[JSON.stringify(params)] = new OldCacheEntry(value); - } - - getIssueListForRepo(org: string, repo: string, params: IssueListParams): IssueListResponse | null { - const repoCache = this.getRepoCache(org, repo); - return this.getCacheValue(repoCache.issueListForRepoCache[JSON.stringify(params)] ?? null); - } - - setIssueListForRepo(org: string, repo: string, params: IssueListParams, value: IssueListResponse): void { - const repoCache = this.getRepoCache(org, repo); - repoCache.issueListForRepoCache[JSON.stringify(params)] = new OldCacheEntry(value); - } - - getPullRequest(org: string, repo: string, pullRequest: number): PullResponse | null { - const repoCache = this.getRepoCache(org, repo); - return this.getCacheValue(repoCache.pullCache[pullRequest] ?? null); - } - - getPullListForRepo(org: string, repo: string, params: PullListParams): PullListResponse | null { - const repoCache = this.getRepoCache(org, repo); - return this.getCacheValue(repoCache.pullListForRepoCache[JSON.stringify(params)] ?? null); - } - - setPullListForRepo(org: string, repo: string, params: PullListParams, value: PullListResponse): void { - const repoCache = this.getRepoCache(org, repo); - repoCache.pullListForRepoCache[JSON.stringify(params)] = new OldCacheEntry(value); - } - - setPullRequest(org: string, repo: string, pullRequest: PullResponse): void { - const pullCache = this.getRepoCache(org, repo).pullCache; - const existingCache = pullCache[pullRequest.number]; - if (existingCache) { - const now = new Date(); - existingCache.created = now; - existingCache.value = pullRequest; - } else { - pullCache[pullRequest.number] = new OldCacheEntry(pullRequest); - } - } - - getIssueQuery(query: string): IssueSearchResponse | null { - return this.getCacheValue(this.queries.issueCache[query] ?? null); - } - - setIssueQuery(query: string, result: IssueSearchResponse): void { - this.queries.issueCache[query] = new OldCacheEntry(result); - } - - getRepoQuery(query: string): RepoSearchResponse | null { - return this.getCacheValue(this.queries.repoCache[query] ?? null); - } - - setRepoQuery(query: string, result: RepoSearchResponse): void { - this.queries.repoCache[query] = new OldCacheEntry(result); - } - - private getOrgCache(org: string): OldOrgCache { - let orgCache = this.orgs[org]; - if (!orgCache) { - orgCache = this.orgs[org] = new OldOrgCache(); - } - return orgCache; - } - - private getRepoCache(org: string, repo: string) { - const orgCache = this.getOrgCache(org); - let repoCache = orgCache.repos[repo]; - if (!repoCache) { - repoCache = orgCache.repos[repo] = new OldRepoCache(); - } - return repoCache; - } - - private getCacheValue(cacheEntry: OldCacheEntry | null): T | null { - if (!cacheEntry || cacheEntry.expired) { - return null; - } else { - return cacheEntry.value; - } - } -} diff --git a/src/github/github.ts b/src/github/github.ts index 4d13e8b..ef2b0ab 100644 --- a/src/github/github.ts +++ b/src/github/github.ts @@ -1,3 +1,10 @@ +import { RequestError, mapObject } from "../util"; + +import type { GithubAccount } from "../settings"; +import { PluginSettings } from "../plugin"; +import { issueListSortFromQuery, pullListSortFromQuery, searchSortFromQuery } from "../query/sort"; +import type { QueryParams } from "../query/types"; +import { GitHubApi } from "./api"; import type { CheckRunListResponse, IssueListParams, @@ -6,20 +13,15 @@ import type { IssueSearchParams, IssueSearchResponse, IssueTimelineResponse, + MaybePaginated, PullListParams, PullListResponse, PullResponse, TimelineCrossReferencedEvent, } from "./response"; -import type { RemoveIndexSignature } from "src/util"; -import { RequestError, sanitizeObject } from "src/util"; -import { GitHubApi } from "./api"; -import { OldCache } from "./cache"; -import type { GithubAccount } from "src/settings"; -import { PluginSettings } from "src/plugin"; +// TODO: Refactor this whole file into a class for better use in dataview queries, etc -const cache = new OldCache(); const tokenMatchRegex = /repo:(.+)\//; const api = new GitHubApi(); @@ -45,133 +47,151 @@ function getToken(org?: string, query?: string): string | undefined { return account?.token; } -export function getIssue(org: string, repo: string, issue: number): Promise { - return api.getIssue(org, repo, issue, getToken(org)); +export function getIssue(org: string, repo: string, issue: number, skipCache = false): Promise { + return api.getIssue(org, repo, issue, getToken(org), skipCache); } -export function getMyIssues(params: IssueListParams, org?: string, skipCache = false): Promise { +export function getMyIssues( + params: QueryParams, + org?: string, + skipCache = false, +): Promise> { const account = getAccount(org); if (!account?.token) { - return Promise.resolve([]); + return Promise.resolve({ meta: {}, response: [] }); } - const _params = sanitizeObject(params, { - assignee: false, - creator: false, - direction: true, - labels: true, - mentioned: false, - milestone: false, - page: true, - per_page: true, - since: true, - sort: true, - state: true, - filter: true, - org: false, - repo: false, - }); + const listParams = mapObject( + params, + { + assignee: true, + creator: true, + direction: true, + labels: (params) => { + if (Array.isArray(params.labels)) { + return params.labels.join(","); + } + return params.labels; + }, + mentioned: true, + page: true, + per_page: true, + since: true, + sort: (params) => issueListSortFromQuery(params), + state: true, + }, + true, + true, + ); - setPageSize(_params); + setPageSize(listParams); - if (Array.isArray(_params.labels)) { - _params.labels = _params.labels.join(","); - } - - return api.listIssuesForToken(_params, account.token); + return api.listIssuesForToken(listParams, account.token, skipCache); } export function getIssuesForRepo( - params: IssueListParams, + params: QueryParams, org: string, repo: string, skipCache = false, -): Promise { - const _params = sanitizeObject(params, { - assignee: true, - creator: true, - direction: true, - labels: true, - mentioned: true, - milestone: true, - page: true, - per_page: true, - since: true, - sort: true, - state: true, - org: false, - repo: false, - filter: false, - }); +): Promise> { + const listParams = mapObject( + params, + { + assignee: true, + creator: true, + direction: true, + labels: (params) => { + if (Array.isArray(params.labels)) { + return params.labels.join(","); + } + return params.labels; + }, + mentioned: true, + page: true, + per_page: true, + since: true, + sort: (params) => issueListSortFromQuery(params), + state: true, + }, + true, + true, + ); - setPageSize(_params); + setPageSize(listParams); - if (Array.isArray(_params.labels)) { - _params.labels = _params.labels.join(","); - } - - return api.listIssuesForRepo(org, repo, _params, getToken(org)); + return api.listIssuesForRepo(org, repo, listParams, getToken(org), skipCache); } -export function getPullRequest(org: string, repo: string, pullRequest: number): Promise { - return api.getPullRequest(org, repo, pullRequest, getToken(org)); +export function getPullRequest( + org: string, + repo: string, + pullRequest: number, + skipCache = false, +): Promise { + return api.getPullRequest(org, repo, pullRequest, getToken(org), skipCache); } -export function getPullRequestsForRepo(params: PullListParams, org: string, repo: string): Promise { - const _params = sanitizeObject(params, { - org: false, - repo: false, - base: true, - direction: true, - head: true, - page: true, - per_page: true, - sort: true, - state: true, - }); +export function getPullRequestsForRepo( + params: QueryParams, + org: string, + repo: string, + skipCache = false, +): Promise> { + const listParams = mapObject( + params, + { + direction: true, + page: true, + per_page: true, + sort: (params) => pullListSortFromQuery(params), + state: true, + }, + true, + true, + ); - setPageSize(_params); - return api.listPullRequestsForRepo(org, repo, _params, getToken(org)); + setPageSize(listParams); + return api.listPullRequestsForRepo(org, repo, listParams, getToken(org), skipCache); } -export function listCheckRunsForRef(org: string, repo: string, ref: string): Promise { - return api.listCheckRunsForRef(org, repo, ref, getToken(org)); +export function listCheckRunsForRef( + org: string, + repo: string, + ref: string, + skipCache = false, +): Promise { + return api.listCheckRunsForRef(org, repo, ref, getToken(org), skipCache); } export async function searchIssues( - params: RemoveIndexSignature, + params: QueryParams, query: string, org?: string, skipCache = false, -): Promise { - const _params = sanitizeObject(params, { - q: false, - baseUrl: false, - headers: false, - mediaType: false, - order: true, - page: true, - per_page: true, - request: false, - sort: true, - }); +): Promise> { + const searchParams = mapObject( + params, + { + q: () => query, + sort: (params) => searchSortFromQuery(params), + order: (params) => params.order, + page: (params) => params.page, + per_page: (params) => params.per_page, + }, + true, + true, + ); - setPageSize(_params); - _params.q = query; - - const cachedResponse = cache.getIssueQuery(query); - if (cachedResponse && !skipCache) { - return Promise.resolve(cachedResponse); - } - - const response = await api.searchIssues(_params, getToken(org, query)); - cache.setIssueQuery(query, response); - return response; + setPageSize(searchParams); + return api.searchIssues(searchParams, getToken(org, query), skipCache); } +// TODO: This is in the wrong place and should be at the API level to be properly cached export async function getPRForIssue(timelineUrl: string, org?: string): Promise { - let response: IssueTimelineResponse | null = null; + let result: IssueTimelineResponse | null = null; try { - response = (await api.queueRequest({ url: timelineUrl }, getToken(org))).json; + const { response } = await api.queueRequest({ url: timelineUrl }, getToken(org)); + result = response.json as IssueTimelineResponse; } catch (err) { // 404 means there's no timeline for this, we can ignore the error if (err instanceof RequestError && err.status === 404) { @@ -180,12 +200,12 @@ export async function getPRForIssue(timelineUrl: string, org?: string): Promise< throw err; } } - if (!response) { + if (!result) { return null; } // TODO: Figure out a better/more reliable way to do this. - const crossRefEvent = response.find((_evt) => { + const crossRefEvent = result.find((_evt) => { const evt = _evt as Partial; return evt.event === "cross-referenced" && evt.source?.issue?.pull_request?.html_url; }) as TimelineCrossReferencedEvent | undefined; diff --git a/src/github/response.ts b/src/github/response.ts index d9fdc3b..2e92924 100644 --- a/src/github/response.ts +++ b/src/github/response.ts @@ -13,6 +13,24 @@ export interface PaginationParams { page?: number; } +export interface LinkMeta { + url: string; + page: number; + per_page: number; +} + +export interface PaginationMeta { + first?: LinkMeta; + prev?: LinkMeta; + next?: LinkMeta; + last?: LinkMeta; +} + +export interface MaybePaginated { + meta: PaginationMeta; + response: T; +} + // Response Types export type IssueResponse = RestEndpointMethodTypes["issues"]["get"]["response"]["data"]; export type IssueListResponse = RestEndpointMethodTypes["issues"]["list"]["response"]["data"]; @@ -52,7 +70,9 @@ export type PullListParams = PaginationParams & { }; export type IssueSearchParams = RestEndpointMethodTypes["search"]["issuesAndPullRequests"]["parameters"]; -export function getSearchResultIssueStatus(issue: IssueSearchResponse["items"][number]): IssueStatus { +export function getSearchResultIssueStatus( + issue: Pick, +): IssueStatus { if (issue.pull_request?.merged_at || issue.state_reason === "completed") { return IssueStatus.Done; } else if (issue.closed_at || issue.state === "closed") { diff --git a/src/inline/inline.ts b/src/inline/inline.ts index f121012..4f46a9d 100644 --- a/src/inline/inline.ts +++ b/src/inline/inline.ts @@ -1,13 +1,13 @@ -import { IssueStatus, getIssueStatus, getPRStatus } from "src/github/response"; -import { getIssue, getPullRequest } from "../github/github"; -import { setIssueIcon, setPRIcon, setPRMergeableIcon } from "src/icon"; - -import type { ParsedUrl } from "../github/url-parse"; -import { PluginSettings } from "src/plugin"; -import type { PullResponse } from "src/github/response"; -import { RequestError } from "src/util"; -import { parseUrl } from "../github/url-parse"; import { setIcon } from "obsidian"; +import { IssueStatus, getIssueStatus, getPRStatus } from "../github/response"; +import { setIssueIcon, setPRIcon, setPRMergeableIcon } from "../icon"; + +import { PluginSettings } from "../plugin"; +import type { PullResponse } from "../github/response"; +import { RequestError } from "../util"; +import { parseUrl } from "../github/url-parse"; +import type { ParsedUrl } from "../github/url-parse"; +import { getIssue, getPullRequest } from "../github/github"; interface TagConfig { icon: HTMLSpanElement; diff --git a/src/plugin.spec.ts b/src/plugin.spec.ts index bc55f84..5f29f13 100644 --- a/src/plugin.spec.ts +++ b/src/plugin.spec.ts @@ -1,10 +1,9 @@ -import { expect, jest, test } from "@jest/globals"; -import { GithubLinkPlugin, PluginData, PluginSettings, getCache } from "./plugin"; -import { beforeEach, describe } from "node:test"; +import { expect, jest, test, describe, beforeEach } from "@jest/globals"; import type { Plugin, RequestUrlResponse } from "obsidian"; import { App } from "obsidian"; +import type { PluginMock } from "../__mocks__/obsidian/Plugin"; import * as manifest from "../manifest.json"; -import type { PluginMock } from "__mocks__/obsidian/Plugin"; +import { GithubLinkPlugin, PluginData, PluginSettings, getCache } from "./plugin"; import type { GithubLinkPluginSettings } from "./settings"; import { DEFAULT_SETTINGS } from "./settings"; import { CacheEntry, RequestCache } from "./github/cache"; @@ -57,7 +56,7 @@ describe("GithubLinkPlugin", () => { test("should load stored cache", async () => { const cacheEntry = new CacheEntry( { url: "mock" }, - { json: "mock" } as RequestUrlResponse, + { json: "mock", headers: {} } as RequestUrlResponse, new Date(), null, null, @@ -76,6 +75,8 @@ describe("GithubLinkPlugin", () => { { stored: { tagTooltips: false }, name: "tagTooltips" }, { stored: { minRequestSeconds: 69 }, name: "minRequestSeconds" }, { stored: { logLevel: LogLevel.Debug }, name: "logLevel" }, + { stored: { showPagination: !DEFAULT_SETTINGS.showPagination }, name: "showPagination" }, + { stored: { showRefresh: !DEFAULT_SETTINGS.showRefresh }, name: "showRefresh" }, ])("should merge stored and default settings ($name)", async ({ stored }) => { plugin = new GithubLinkPlugin(app, manifest); mockedPlugin(plugin).data = { settings: stored }; diff --git a/src/plugin.ts b/src/plugin.ts index 2053a14..3c32dcc 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1,12 +1,14 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +import { Plugin } from "obsidian"; import { DEFAULT_SETTINGS, GithubLinkPluginSettingsTab } from "./settings"; import { Logger } from "./logger"; import type { GithubLinkPluginData, GithubLinkPluginSettings } from "./settings"; import { InlineRenderer } from "./inline/inline"; -import { Plugin } from "obsidian"; -import { QueryProcessor } from "./query/processor"; import { createInlineViewPlugin } from "./inline/view-plugin"; import { RequestCache } from "./github/cache"; +import { QueryProcessor } from "./query/processor"; export const PluginSettings: GithubLinkPluginSettings = { ...DEFAULT_SETTINGS }; export const PluginData: GithubLinkPluginData = { cache: null, settings: PluginSettings }; @@ -33,6 +35,8 @@ export class GithubLinkPlugin extends Plugin { tagShowPRMergeable: data.tagShowPRMergeable ?? PluginSettings.tagShowPRMergeable, tagTooltips: data.tagTooltips ?? PluginSettings.tagTooltips, defaultAccount: data.defaultAccount ?? PluginSettings.defaultAccount, + showPagination: data.showPagination ?? PluginSettings.showPagination, + showRefresh: data.showRefresh ?? PluginSettings.showRefresh, }; const newData: GithubLinkPluginData = { cache: data.cache ?? PluginData.cache, diff --git a/src/query/column/base.ts b/src/query/column/base.ts index b8c20cc..e795106 100644 --- a/src/query/column/base.ts +++ b/src/query/column/base.ts @@ -1,13 +1,16 @@ -import { parseUrl, repoAPIToBrowserUrl } from "src/github/url-parse"; +/* eslint-disable @typescript-eslint/no-unsafe-assignment */ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +import { parseUrl, repoAPIToBrowserUrl } from "../../github/url-parse"; -import { DateFormat } from "src/util"; -import type { IssueSearchResponse } from "src/github/response"; +import { DateFormat } from "../../util"; +import type { IssueListResponse } from "../../github/response"; +import type { TableResult } from "../types"; export interface ColumnGetter { header: string; cell: (row: T, el: HTMLTableCellElement) => void | Promise; } -export type ColumnsMap = Record>; +export type ColumnsMap = Record>; export function DateCell(value: string | undefined | null, el: HTMLTableCellElement) { el.classList.add("github-link-table-date"); @@ -28,7 +31,7 @@ export function DateCell(value: string | undefined | null, el: HTMLTableCellElem /** * Issue and PR columns share types, so some columns are shared */ -export const CommonIssuePRColumns: ColumnsMap = { +export const CommonIssuePRColumns: ColumnsMap = { number: { header: "Number", cell: (row, el) => { @@ -40,7 +43,7 @@ export const CommonIssuePRColumns: ColumnsMap { el.classList.add("github-link-table-repo"); - const url = repoAPIToBrowserUrl(row.repository_url); + const url = repoAPIToBrowserUrl((row as IssueListResponse[number]).repository_url); const parsed = parseUrl(url); el.createEl("a", { text: parsed.repo, href: url, attr: { target: "_blank" } }); }, diff --git a/src/query/column/defaults.ts b/src/query/column/defaults.ts index ffe5c9a..5a74de6 100644 --- a/src/query/column/defaults.ts +++ b/src/query/column/defaults.ts @@ -1,7 +1,21 @@ -import { QueryType } from "../params"; +import type { QueryType } from "../types"; +import { IssueColumns } from "./issue"; +import { PullRequestColumns } from "./pull-request"; +import { RepoColumns } from "./repo"; + +/* +We have to do this workaround with the QueryType enum because ts-jest does not support +using constant enums like this; see https://github.com/kulshekhar/ts-jest/pull/308/files +*/ export const DEFAULT_COLUMNS = { - [QueryType.Issue]: ["number", "title", "author", "created", "status"], - [QueryType.PullRequest]: ["number", "title", "author", "created", "status"], - [QueryType.Repo]: [], + ["issue" as QueryType]: ["number", "title", "author", "created", "status"], + ["pull-request" as QueryType]: ["number", "title", "author", "created", "status"], + ["repo" as QueryType]: [], +}; + +export const ALL_COLUMNS = { + ["issue" as QueryType]: IssueColumns, + ["pull-request" as QueryType]: PullRequestColumns, + ["repo" as QueryType]: RepoColumns, }; diff --git a/src/query/column/issue.ts b/src/query/column/issue.ts index 2e3690e..d7f0833 100644 --- a/src/query/column/issue.ts +++ b/src/query/column/issue.ts @@ -1,11 +1,12 @@ -import { getSearchResultIssueStatus, type IssueSearchResponse } from "src/github/response"; +import type { IssueListResponse, IssueSearchResponse } from "../../github/response"; +import { getSearchResultIssueStatus } from "../../github/response"; +import { setIssueIcon } from "../../icon"; +import { titleCase } from "../../util"; +import { createTag } from "../../inline/inline"; +import { getPRForIssue } from "../../github/github"; import { CommonIssuePRColumns, type ColumnsMap } from "./base"; -import { setIssueIcon } from "src/icon"; -import { titleCase } from "src/util"; -import { createTag } from "src/inline/inline"; -import { getPRForIssue } from "src/github/github"; -export const IssueColumns: ColumnsMap = { +export const IssueColumns: ColumnsMap = { ...CommonIssuePRColumns, status: { header: "Status", @@ -14,12 +15,18 @@ export const IssueColumns: ColumnsMap = { const status = getSearchResultIssueStatus(row); const icon = wrapper.createSpan({ cls: "github-link-status-icon" }); setIssueIcon(icon, status); - wrapper.createSpan({ text: row.state_reason === "not_planned" ? "Not Planned" : titleCase(status) }); + wrapper.createSpan({ + text: + (row as IssueSearchResponse["items"][number]).state_reason === "not_planned" + ? "Not Planned" + : titleCase(status), + }); }, }, pr: { header: "PR", - cell: async (row, el) => { + cell: async (_row, el) => { + const row = _row as IssueListResponse[number]; // TODO: Figure out how to include org here for private repos if (!row.timeline_url) { return; diff --git a/src/query/column/pull-request.ts b/src/query/column/pull-request.ts index bef9083..d8e04a2 100644 --- a/src/query/column/pull-request.ts +++ b/src/query/column/pull-request.ts @@ -1,9 +1,9 @@ -import { getSearchResultIssueStatus, IssueStatus, type IssueSearchResponse } from "src/github/response"; +import { getSearchResultIssueStatus, IssueStatus } from "../../github/response"; +import { setPRIcon } from "../../icon"; +import { titleCase } from "../../util"; import { CommonIssuePRColumns, type ColumnsMap } from "./base"; -import { setPRIcon } from "src/icon"; -import { titleCase } from "src/util"; -export const PullRequestColumns: ColumnsMap = { +export const PullRequestColumns: ColumnsMap = { ...CommonIssuePRColumns, status: { header: "Status", diff --git a/src/query/column/repo.ts b/src/query/column/repo.ts index 1299daf..0dfbaef 100644 --- a/src/query/column/repo.ts +++ b/src/query/column/repo.ts @@ -1,4 +1,3 @@ -import type { RepoSearchResponse } from "src/github/response"; import type { ColumnsMap } from "./base"; -export const RepoColumns: ColumnsMap = {}; +export const RepoColumns: ColumnsMap = {}; diff --git a/src/query/output.ts b/src/query/output.ts deleted file mode 100644 index a653479..0000000 --- a/src/query/output.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { getProp, titleCase } from "src/util"; - -import type { BaseParams } from "./params"; -import { DEFAULT_COLUMNS } from "./column/defaults"; -import { IssueColumns } from "./column/issue"; -import { PullRequestColumns } from "./column/pull-request"; -import { QueryType } from "./params"; -import { RepoColumns } from "./column/repo"; -import { setIcon } from "obsidian"; - -const ALL_COLUMNS = { - [QueryType.PullRequest]: PullRequestColumns, - [QueryType.Issue]: IssueColumns, - [QueryType.Repo]: RepoColumns, -}; - -export function renderTable( - params: BaseParams, - result: T, - el: HTMLElement, - renderFn: (element: HTMLElement, skipCache?: boolean) => Promise, - externalLink?: string, -) { - el.empty(); - const tableWrapper = el.createDiv({ cls: "github-link-table-wrapper" }); - const tableScrollWrapper = tableWrapper.createDiv({ cls: "github-link-table-scroll-wrapper" }); - const table = tableScrollWrapper.createEl("table", { cls: "github-link-table" }); - - if (params.refresh) { - const refresh = tableWrapper.createDiv({ cls: "github-link-table-refresh" }); - if (externalLink) { - refresh.createEl("a", { - cls: "github-link-table-refresh-external-link", - text: "View on GitHub", - href: externalLink, - attr: { target: "_blank" }, - }); - } - const refreshButton = refresh.createEl("button", { - cls: "clickable-icon", - attr: { "aria-label": "Refresh Results" }, - }); - refreshButton.addEventListener("click", () => { - void renderFn(el, true); - }); - setIcon(refreshButton, "refresh-cw"); - } - - const thead = table.createEl("thead"); - let columns = params.columns; - if (!columns || columns.length === 0) { - columns = DEFAULT_COLUMNS[params.queryType]; - } - - // Ensure columns are lowercase - columns = columns.map((c) => c.toLowerCase()); - - for (const col of columns) { - const th = thead.createEl("th"); - // Get predefined header if available - th.setText(ALL_COLUMNS[params.queryType][col]?.header ?? titleCase(col)); - } - const tbody = table.createEl("tbody"); - const items = Array.isArray(result) ? result : result.items; - for (const row of items) { - const tr = tbody.createEl("tr"); - for (const col of columns) { - const cell = tr.createEl("td"); - const renderer = ALL_COLUMNS[params.queryType][col]; - if (renderer) { - void renderer.cell(row, cell); - } else { - const cellVal = getProp(row, col); - if (cellVal !== null) { - cell.setText(typeof cellVal === "string" ? cellVal : JSON.stringify(cellVal)); - } else { - cell.setText(""); - } - } - } - } -} diff --git a/src/query/params.ts b/src/query/params.ts index b94a9b0..e69de29 100644 --- a/src/query/params.ts +++ b/src/query/params.ts @@ -1,47 +0,0 @@ -import type { IssueListParams, PullListParams } from "src/github/response"; - -import { parseYaml } from "obsidian"; - -export enum OutputType { - Table = "table", -} - -export enum QueryType { - PullRequest = "pull-request", - Issue = "issue", - Repo = "repo", -} - -export interface BaseParams { - outputType: OutputType; - queryType: QueryType; - columns: string[]; - refresh?: boolean; -} - -export type TableQueryParams = BaseParams & { query: string; org?: string } & T & { q: never }; -export type TableParams = T & BaseParams; - -export function processParams(source: string): BaseParams | null { - let params: BaseParams; - try { - params = parseYaml(source); - } catch (e) { - console.error(`Github Link: YAML Parsing failed, attempting simplistic parsing\n${e}`); - params = Object.fromEntries(source.split("\n").map((l) => l.split(/:\s?/))); - } - - params.refresh = params.refresh ?? true; - - return params ?? null; -} - -export function isTableQueryParams(params: BaseParams): params is TableQueryParams { - return params.outputType === OutputType.Table && Boolean((params as TableQueryParams).query); -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function isTableParams(params: BaseParams): params is TableParams { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return params.outputType === OutputType.Table && !(params as TableQueryParams).query; -} diff --git a/src/query/processor.ts b/src/query/processor.ts index 9dc4c35..dbd1e0e 100644 --- a/src/query/processor.ts +++ b/src/query/processor.ts @@ -1,50 +1,11 @@ -import { type MarkdownPostProcessorContext } from "obsidian"; -import type { TableParams, TableQueryParams } from "./params"; -import { QueryType, isTableParams, isTableQueryParams, processParams } from "./params"; -import { renderTable } from "./output"; -import { searchIssues, getIssuesForRepo, getMyIssues, getPullRequestsForRepo } from "src/github/github"; -import type { IssueListParams, IssueSearchParams, PullListParams } from "src/github/response"; +import type { MarkdownPostProcessorContext } from "obsidian"; +import { GithubQuery } from "./query"; export async function QueryProcessor( source: string, el: HTMLElement, _ctx: MarkdownPostProcessorContext, ): Promise { - const params = processParams(source); - - if (!params) { - // TODO: show an error instead - el.setText(source); - return; - } - - const renderFn = async (element: HTMLElement, skipCache = false) => { - let response: { items: unknown[] } | unknown[] | undefined = undefined; - let externalLink: string | undefined; - if (isTableQueryParams(params)) { - if (params.queryType === QueryType.Issue || params.queryType === QueryType.PullRequest) { - const queryParams = params as TableQueryParams; - response = await searchIssues(params, params.query, queryParams.org, skipCache); - externalLink = `https://github.com/search?q=${encodeURIComponent(params.query)}`; - } - } else if (isTableParams(params)) { - if (params.queryType === QueryType.Issue) { - const issueParams = params as TableParams; - if (issueParams.org && issueParams.repo) { - response = await getIssuesForRepo(issueParams, issueParams.org, issueParams.repo, skipCache); - } else { - response = await getMyIssues(issueParams, issueParams.org, skipCache); - } - } else if (params.queryType === QueryType.PullRequest) { - const pullParams = params as TableParams; - if (pullParams.org && pullParams.repo) { - response = await getPullRequestsForRepo(pullParams, pullParams.org, pullParams.repo); - } - } - } - if (response) { - renderTable(params, response, element, renderFn, externalLink); - } - }; - await renderFn(el); + const query = new GithubQuery(el); + await query.init(source); } diff --git a/src/query/query.ts b/src/query/query.ts new file mode 100644 index 0000000..68a796c --- /dev/null +++ b/src/query/query.ts @@ -0,0 +1,225 @@ +import { parseYaml, setIcon } from "obsidian"; +import { searchIssues, getIssuesForRepo, getMyIssues, getPullRequestsForRepo } from "../github/github"; +import type { MaybePaginated, PaginationMeta } from "../github/response"; +import { PluginSettings } from "../plugin"; +import { getProp, isEqual, titleCase } from "../util"; +import { ALL_COLUMNS, DEFAULT_COLUMNS } from "./column/defaults"; +import type { QueryParams, TableResult } from "./types"; +import { OutputType, QueryType } from "./types"; + +export class GithubQuery { + private params!: QueryParams; + private result: TableResult | null = null; + private resultMeta: PaginationMeta | null = null; + + constructor(private readonly hostElement: HTMLElement) {} + + public async init(source: string): Promise { + const parsedParams = this.parseCodeblock(source); + if (!parsedParams) { + console.error(`Github Link: simplistic parsing failed`); + } else { + await this.setParams(parsedParams); + } + } + + /** + * Setting the parameters triggers calling the API + */ + public async setParams(newParams: QueryParams = this.params, forceUpdate = false): Promise { + const currentParams = this.params; + this.params = newParams; + if (forceUpdate || !isEqual(currentParams, newParams)) { + const result = await this.executeQuery(forceUpdate); + if (result) { + this.setResult(result.response, result.meta); + } + } + } + + public setResult(result: TableResult, meta: PaginationMeta): void { + this.result = result; + this.resultMeta = meta; + this.render(); + } + + public parseCodeblock(source: string): QueryParams | null { + let params: QueryParams | null; + try { + params = parseYaml(source) as QueryParams; + } catch (e) { + console.error(`Github Link: YAML Parsing failed, attempting simplistic parsing`); + console.error(e); + params = Object.fromEntries(source.split("\n").map((l) => l.split(/:\s?/))) as QueryParams; + } + return params ?? null; + } + + public async executeQuery(skipCache = false): Promise | null> { + const params = this.params; + if (params.outputType === OutputType.Table) { + // Custom Query + if (params.query && (params.queryType === QueryType.Issue || params.queryType === QueryType.PullRequest)) { + const { meta, response } = await searchIssues(params, params.query, params.org, skipCache); + return { meta, response: response.items }; + } + // Issue query with org and repo provided + else if (params.queryType === QueryType.Issue && params.org && params.repo) { + return await getIssuesForRepo(params, params.org, params.repo, skipCache); + } + // Issue query without org or repo provided + else if (params.queryType === QueryType.Issue) { + return await getMyIssues(params, params.org, skipCache); + } + // Pull request query with org and repo provided + else if (params.queryType === QueryType.PullRequest && params.org && params.repo) { + return await getPullRequestsForRepo(params, params.org, params.repo, skipCache); + } + } + return null; + } + + public render(): void { + if (!this.result) { + throw new Error("Attempted to render table before there was a result."); + } + + this.hostElement.empty(); + const tableWrapper = this.hostElement.createDiv({ cls: "github-link-table-wrapper" }); + const tableScrollWrapper = tableWrapper.createDiv({ cls: "github-link-table-scroll-wrapper" }); + const table = tableScrollWrapper.createEl("table", { cls: "github-link-table" }); + + const queryType = this.params.queryType; + + // Use default columns if none are provided + let columns = this.params.columns; + if (!columns || columns.length === 0) { + columns = DEFAULT_COLUMNS[queryType]; + } + + // Ensure columns are lowercase + columns = columns.map((c) => c.toLowerCase()); + + // Render + this.renderFooter(this.params, this.result, this.resultMeta, tableWrapper); + this.renderHeader(table, queryType, columns); + this.renderBody(table, queryType, columns, this.result); + } + + private renderHeader(table: HTMLTableElement, queryType: QueryType, columns: string[]): void { + const thead = table.createEl("thead"); + for (const col of columns) { + const th = thead.createEl("th"); + // Get predefined header if available, otherwise try and create a title + th.setText(ALL_COLUMNS[queryType][col]?.header ?? titleCase(col)); + } + } + + private renderBody(table: HTMLTableElement, queryType: QueryType, columns: string[], result: TableResult): void { + const tbody = table.createEl("tbody"); + for (const row of result) { + const tr = tbody.createEl("tr"); + for (const col of columns) { + this.renderCell(tr, queryType, col, row); + } + } + } + + private renderCell(tr: HTMLTableRowElement, queryType: QueryType, column: string, row: TableResult[number]): void { + const cell = tr.createEl("td"); + const renderer = ALL_COLUMNS[queryType][column]; + if (renderer) { + void renderer.cell(row, cell); + } else { + const cellVal = getProp(row, column); + if (cellVal !== null) { + cell.setText(typeof cellVal === "string" ? cellVal : JSON.stringify(cellVal)); + } else { + cell.setText(""); + } + } + } + + private renderFooter( + params: QueryParams, + result: TableResult, + meta: PaginationMeta | null, + parent: HTMLElement, + ): void { + const footer = parent.createDiv({ cls: "github-link-table-footer" }); + + // Add external link to footer if available + const externalLink = this.getExternalLink(params); + if (externalLink) { + footer.createEl("a", { + cls: "github-link-table-footer-external-link", + text: "View on GitHub", + href: externalLink, + attr: { target: "_blank" }, + }); + } + + this.renderPagination(meta, footer); + + if (PluginSettings.showRefresh) { + const refreshButton = footer.createEl("button", { + cls: "clickable-icon", + attr: { "aria-label": "Refresh Results" }, + }); + refreshButton.addEventListener("click", () => { + void this.setParams(this.params, true); + }); + setIcon(refreshButton, "refresh-cw"); + } + } + + private renderPagination(meta: PaginationMeta | null, parent: HTMLElement): void { + if (PluginSettings.showPagination && this.hasSomeRel(meta)) { + const pagination = parent.createDiv({ cls: "github-link-table-pagination" }); + + // First, previous + if (meta?.first && (!meta.prev || meta.prev.page !== meta.first.page)) { + const first = pagination.createEl("a", { text: "<<", href: "#", attr: { role: "button" } }); + first.addEventListener("click", () => { + void this.setParams({ ...this.params, page: meta.first?.page }); + }); + } + if (meta?.prev) { + const prev = pagination.createEl("a", { text: meta.prev.page.toString(), href: "#", attr: { role: "button" } }); + prev.addEventListener("click", () => { + void this.setParams({ ...this.params, page: meta.prev?.page }); + }); + } + + // Current Page + pagination.createSpan({ text: (this.params.page ?? 1).toString() }); + + // Next, last + if (meta?.next) { + const next = pagination.createEl("a", { text: meta.next.page.toString(), href: "#", attr: { role: "button" } }); + next.addEventListener("click", () => { + void this.setParams({ ...this.params, page: meta.next?.page }); + }); + } + if (meta?.last && (!meta.next || meta.next.page !== meta.last.page)) { + const last = pagination.createEl("a", { text: ">>", href: "#", attr: { role: "button" } }); + last.addEventListener("click", () => { + void this.setParams({ ...this.params, page: meta.last?.page }); + }); + } + } + } + + private hasSomeRel(meta: PaginationMeta | null): boolean { + return Boolean(meta && (meta.first || meta.prev || meta.next || meta.last)); + } + + private getExternalLink(params: QueryParams): string | null { + // Custom search query + if (params.query && (params.queryType === QueryType.Issue || params.queryType === QueryType.PullRequest)) { + return `https://github.com/search?q=${encodeURIComponent(params.query)}`; + } else { + return null; + } + } +} diff --git a/src/query/sort.ts b/src/query/sort.ts new file mode 100644 index 0000000..f12a2b0 --- /dev/null +++ b/src/query/sort.ts @@ -0,0 +1,32 @@ +import type { IssueSearchParams, IssueListParams, PullListParams } from "../github/response"; +import type { QueryParams } from "./types"; + +/** + * Utility function to transform generic param sort into sort for issue search + */ +export function searchSortFromQuery(params: QueryParams): IssueSearchParams["sort"] { + if (params.sort !== "popularity" && params.sort !== "long-running") { + return params.sort; + } + return undefined; +} + +/** + * Utility function to transform generic param sort into sort for issue list + */ +export function issueListSortFromQuery(params: QueryParams): IssueListParams["sort"] { + if (params.sort && ["created", "updated", "comments"].includes(params.sort)) { + return params.sort as IssueListParams["sort"]; + } + return undefined; +} + +/** + * Utility function to transform generic param sort into sort for pull list + */ +export function pullListSortFromQuery(params: QueryParams): PullListParams["sort"] { + if (params.sort && ["created", "updated", "popularity", "long-running"].includes(params.sort)) { + return params.sort as PullListParams["sort"]; + } + return undefined; +} diff --git a/src/query/types.ts b/src/query/types.ts new file mode 100644 index 0000000..872ac40 --- /dev/null +++ b/src/query/types.ts @@ -0,0 +1,102 @@ +import type { IssueListResponse, IssueSearchResponse, PullListResponse } from "../github/response"; + +export type TableResult = IssueSearchResponse["items"] | IssueListResponse | PullListResponse; + +export enum OutputType { + Table = "table", +} + +export enum QueryType { + PullRequest = "pull-request", + Issue = "issue", + Repo = "repo", +} + +export interface BaseParams { + refresh?: boolean; +} + +/** + * Not all fields are supported by all query types + */ +export interface QueryParams { + outputType: OutputType; + queryType: QueryType; + columns: string[]; + + /** + * Custom query. This will override most other options. + */ + query?: string; + + /** + * Pagination page size + */ + per_page?: number; + + /** + * Pagination page number + */ + page?: number; + + /** + * Repository name + */ + repo?: string; + + /** + * Organization or user name + */ + org?: string; + milestone?: string; + state?: "open" | "closed" | "all"; + assignee?: "none" | "*" | string; + creator?: string; + mentioned?: string; + labels?: string | string[]; + + /** + * "comments" - Issues only + * "popularity", "long-running" - Pull requests only + * "reactions" and "interactions" - Search only + */ + sort?: + | "created" + | "updated" + | "comments" + | "popularity" + | "long-running" + | "reactions" + | "reactions-+1" + | "reactions--1" + | "reactions-smile" + | "reactions-thinking_face" + | "reactions-heart" + | "reactions-tada" + | "interactions"; + + /** + * Sort direction, for most queries + */ + direction?: "asc" | "desc"; + /** + * Sort direction, for search queries + */ + order?: "asc" | "desc"; + since?: string; + + /** + * Issue filter type + */ + filter?: "assigned" | "created" | "mentioned" | "subscribed" | "repos" | "all"; + + /** + * Pull request branch + */ + head?: string; + + /** + * Pull request target + */ + base?: string; +} diff --git a/src/settings/account.ts b/src/settings/account.ts index a35b55d..002dada 100644 --- a/src/settings/account.ts +++ b/src/settings/account.ts @@ -1,9 +1,9 @@ import type { App } from "obsidian"; import { Setting } from "obsidian"; -import type { GithubAccount } from "./types"; -import { AuthModal } from "src/auth-modal"; -import { auth } from "src/github/auth"; import type { Verification } from "@octokit/auth-oauth-device/dist-types/types"; +import { AuthModal } from "../auth-modal"; +import { auth } from "../github/auth"; +import type { GithubAccount } from "./types"; export class AccountSettings { authModal: AuthModal | null = null; diff --git a/src/settings/settings-tab.ts b/src/settings/settings-tab.ts index 31427ce..5f91771 100644 --- a/src/settings/settings-tab.ts +++ b/src/settings/settings-tab.ts @@ -95,6 +95,28 @@ export class GithubLinkPluginSettingsTab extends PluginSettingTab { }); }); + new Setting(containerEl) + .setName("Show refresh button") + .setDesc("Add a refresh button to tables to manually skip the cache.") + .addToggle((toggle) => { + toggle.setValue(PluginSettings.showRefresh); + toggle.onChange((value) => { + PluginSettings.showRefresh = value; + void this.saveSettings(); + }); + }); + + new Setting(containerEl) + .setName("Show pagination") + .setDesc("For query results with more than a single page of results, show pagination controls below the table.") + .addToggle((toggle) => { + toggle.setValue(PluginSettings.showPagination); + toggle.onChange((value) => { + PluginSettings.showPagination = value; + void this.saveSettings(); + }); + }); + new Setting(containerEl) .setName("Status tooltips") .setDesc("Add a tooltip to issue and pull request status icons with status text") diff --git a/src/settings/types.ts b/src/settings/types.ts index 0d89dbb..56d5c19 100644 --- a/src/settings/types.ts +++ b/src/settings/types.ts @@ -18,6 +18,8 @@ export interface GithubLinkPluginSettings { accounts: GithubAccount[]; defaultAccount?: string; defaultPageSize: number; + showPagination: boolean; + showRefresh: boolean; logLevel: LogLevel; tagTooltips: boolean; tagShowPRMergeable: boolean; @@ -29,6 +31,8 @@ export interface GithubLinkPluginSettings { export const DEFAULT_SETTINGS: GithubLinkPluginSettings = { accounts: [], defaultPageSize: 10, + showPagination: true, + showRefresh: true, logLevel: LogLevel.Error, tagTooltips: false, tagShowPRMergeable: false, diff --git a/src/util.ts b/src/util.ts index 95872ab..6e01f9d 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,7 +1,16 @@ +import { isEqual as lodashIsEqual } from "lodash"; + export type RemoveIndexSignature = { [K in keyof T as string extends K ? never : number extends K ? never : symbol extends K ? never : K]: T[K]; }; +/** + * Small wrapper around lodash isEqual to tell typescript to stop complaining about its any types + */ +export function isEqual(value: T, other: R): boolean { + return lodashIsEqual(value, other); +} + export function titleCase(value: string): string { const words = value.split(/[-_]/); return words.map((w) => w.charAt(0)?.toUpperCase() + w.slice(1)).join(" "); @@ -17,6 +26,55 @@ export function assertDefined(value: T | undefined | null): asserts value is } } +type SourceToTargetFunction = (value: T) => R[K]; +type SourceToTarget = K extends keyof T + ? T[K] extends R[K] + ? true | SourceToTargetFunction + : SourceToTargetFunction + : SourceToTargetFunction; +type ObjectMapConfig = { [K in keyof R]: SourceToTarget }; + +/** + * Create an object of type R from source object of type T + * @param config An object describing how to map the values from the source to the new object. + * Using true for a key passes the value from the source object as-is (if possible), + * while a function will be called to transform the property. Config must include + * every non-optional key on the target object type. + */ +export function mapObject( + value: T, + config: ObjectMapConfig, + removeUndefined = false, + removeNull = false, +): R { + const resultEntries: [string, unknown][] = []; + + for (const entry of Object.entries(config)) { + const key = entry[0]; + const configSet = entry[1] as SourceToTarget; + + if (configSet === true) { + resultEntries.push([key, value[key as keyof T]]); + } else { + resultEntries.push([key, configSet(value)]); + } + } + + let finalEntries: [string, unknown][] = []; + if (removeUndefined || removeNull) { + for (const [key, value] of resultEntries) { + if ((removeUndefined && value === undefined) || (removeNull && value === null)) { + continue; + } + finalEntries.push([key, value]); + } + } else { + finalEntries = [...resultEntries]; + } + + return Object.fromEntries(finalEntries) as R; +} + export function sanitizeObject(params: T, usableFieldMap: Record): T { const usableFields: (keyof T)[] = Object.entries(usableFieldMap) .filter(([_, value]) => value) @@ -57,6 +115,7 @@ export function safeJSONParse(value: string, props: Record) // Handle parsing with try / catch let parsed: T; try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment parsed = JSON.parse(value); } catch (err) { return null; @@ -68,6 +127,7 @@ export function safeJSONParse(value: string, props: Record) for (const [_prop, include] of Object.entries(props)) { const prop = _prop as keyof T; if (include && parsed[prop]) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access result[prop] = parsed[prop]; } } @@ -128,9 +188,9 @@ export class RequestError implements Error { this.message = originalError.message; // Request props - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access this.headers = (originalError as any).headers; - // eslint-disable-next-line @typescript-eslint/no-explicit-any + // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access this.status = (originalError as any).status; } } diff --git a/styles.css b/styles.css index cbc7ba8..a7a1e55 100644 --- a/styles.css +++ b/styles.css @@ -281,21 +281,22 @@ body.theme-dark { white-space: nowrap; } -.github-link-table-refresh { +.github-link-table-footer { display: flex; - justify-content: flex-end; + justify-content: space-between; align-items: center; } -.github-link-table-refresh:has(a) { - justify-content: space-between; +.github-link-table-pagination { + display: flex; + gap: 2px; } -.github-link-table-refresh-external-link { +.github-link-table-footer-external-link { font-size: 0.75rem; } -.github-link-table-refresh > button > svg { +.github-link-table-footer > button > svg { width: var(--icon-s); height: var(--icon-s); stroke-width: var(--icon-s-stroke-width); diff --git a/tsconfig.json b/tsconfig.json index fa59e7b..ee12f13 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,5 @@ { "compilerOptions": { - "baseUrl": ".", "inlineSourceMap": true, "inlineSources": true, "module": "ESNext",