From 2b7c9cc0035eae8adf4794116124c44da23d8cf0 Mon Sep 17 00:00:00 2001 From: Dan Pickett Date: Mon, 30 Nov 2020 09:10:45 -0500 Subject: [PATCH] support Jira "Next-gen" projects for story import --- .gitignore | 3 +- package.json | 3 +- src/cli/jira-test.ts | 41 +++++++++ src/cli/push.ts | 89 ++++++++++--------- src/configuration.ts | 6 ++ src/import-adapters/clubhouse/Client.ts | 2 +- .../clubhouse/StorySetImport.ts | 2 +- src/import-adapters/jira/Client.ts | 49 ++++++++++ src/import-adapters/jira/StorySetImport.ts | 65 ++++++++++++++ src/jira-client/Client.ts | 40 +++++++++ src/jira-client/ClientObject.ts | 9 ++ src/jira-client/Configuration.ts | 10 +++ src/jira-client/Issue.ts | 73 +++++++++++++++ src/jira-client/IssueCreationPayload.ts | 7 ++ src/jira-client/IssueListResponse.ts | 7 ++ src/jira-client/IssueResponse.ts | 8 ++ src/jira-client/IssueType.ts | 7 ++ src/jira-client/Project.ts | 7 ++ yarn.lock | 51 +++++++---- 19 files changed, 418 insertions(+), 61 deletions(-) create mode 100644 src/cli/jira-test.ts create mode 100644 src/import-adapters/jira/Client.ts create mode 100644 src/import-adapters/jira/StorySetImport.ts create mode 100644 src/jira-client/Client.ts create mode 100644 src/jira-client/ClientObject.ts create mode 100644 src/jira-client/Configuration.ts create mode 100644 src/jira-client/Issue.ts create mode 100644 src/jira-client/IssueCreationPayload.ts create mode 100644 src/jira-client/IssueListResponse.ts create mode 100644 src/jira-client/IssueResponse.ts create mode 100644 src/jira-client/IssueType.ts create mode 100644 src/jira-client/Project.ts diff --git a/.gitignore b/.gitignore index dfa6cf6..90c78b4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules lib yarn-error.log -coverage \ No newline at end of file +coverage +.env* diff --git a/package.json b/package.json index ccbd53d..b6210f9 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "@types/node": "^14.0.12", "clubhouse-lib": "^0.10.0", "commander": "^5.1.0", - "got": "^11.3.0", + "got": "^11.8.0", + "md-to-adf": "^0.6.4", "mdast-builder": "^1.1.1", "mdast-util-to-string": "^1.1.0", "node-emoji": "^1.10.0", diff --git a/src/cli/jira-test.ts b/src/cli/jira-test.ts new file mode 100644 index 0000000..96c2ce7 --- /dev/null +++ b/src/cli/jira-test.ts @@ -0,0 +1,41 @@ +import { Client } from "../jira-client/Client" +// import { Issue } from "../jira-client/Issue" +// import { IssueType } from "../jira-client/IssueType"; +// import { Board } from "../jira-client/Project"; +// import {inspect} from "util" +import { Client as ImportClient } from "../import-adapters/jira/Client" + +const client = new Client({ + host: process.env.JIRA_HOST || "", + projectKey: "CURR", + authentication: { + basic: { + email: process.env.JIRA_EMAIL || "", + password: process.env.JIRA_API_TOKEN || "" + } + } +}); + + +(async () => { + // const board = new Board(client); + // console.log(await board.getList()) + + // const issueType = new IssueType(client) + // console.log(await issueType.getList()) + + + // const issue = new Issue(client) + // console.log(await issue.getList("type = Epic")) + + // const issue = new Issue(client) + // console.log(inspect(await issue.getCreateMetadata(), {depth: 100})) + // await issue.create({ + // title: "Test Epic", + // description: "* do you make\n* appropriate bullets\n* for formatting", + // issueTypeId: "10032", + // projectId: "10015" + // }) + const importClient = new ImportClient(client) + console.log(await importClient.listEpics()) +})() diff --git a/src/cli/push.ts b/src/cli/push.ts index 639d05b..94a4aa4 100644 --- a/src/cli/push.ts +++ b/src/cli/push.ts @@ -1,56 +1,58 @@ import { Command } from "commander"; -import fs from "fs"; +// import fs from "fs"; import chalk from "chalk"; import emoji from "node-emoji"; import StoryParser from "../markdown/StoryParser"; -import Client from "../import-adapters/clubhouse/Client"; +import { Client as ClubhouseClient } from "../import-adapters/clubhouse/Client"; +import { Client as JiraClient } from "../import-adapters/jira/Client" import configuration from "../configuration"; import normalizeStorySet from "../markdown/normalizeStorySet"; -import StorySetImport from "../import-adapters/clubhouse/StorySetImport"; +import { StorySetImport as ClubhouseStorySetImport } from "../import-adapters/clubhouse/StorySetImport"; +import { StorySetImport as JiraStorySetImport } from "../import-adapters/jira/StorySetImport"; -type PrecheckError = { - message: string; -}; +// type PrecheckError = { +// message: string; +// }; -const fileExists = (markdownFile): PrecheckError | undefined => { - if (!fs.existsSync(markdownFile)) { - return { - message: "File not found.", - }; - } - return undefined; -}; +// const fileExists = (markdownFile): PrecheckError | undefined => { +// if (!fs.existsSync(markdownFile)) { +// return { +// message: "File not found.", +// }; +// } +// return undefined; +// }; -const clubhouseTokenSet = (): PrecheckError | undefined => { - if (configuration.clubhouse.apiToken?.trim() === "") { - return { - message: "Clubhouse Token not set.", - }; - } - return undefined; -}; +// const clubhouseTokenSet = (): PrecheckError | undefined => { +// if (configuration.clubhouse.apiToken?.trim() === "") { +// return { +// message: "Clubhouse Token not set.", +// }; +// } +// return undefined; +// }; -const clubhouseProjectSet = (): PrecheckError | undefined => { - if (!configuration.clubhouse.projectId) { - return { - message: "Clubhouse Project ID not set.", - }; - } - return undefined; -}; +// const clubhouseProjectSet = (): PrecheckError | undefined => { +// if (!configuration.clubhouse.projectId) { +// return { +// message: "Clubhouse Project ID not set.", +// }; +// } +// return undefined; +// }; -const pushPrechecks = [fileExists, clubhouseTokenSet, clubhouseProjectSet]; +// const pushPrechecks = [fileExists, clubhouseTokenSet, clubhouseProjectSet]; const processPush = async (markdownFile: string) => { let errors: string[] = []; - errors = pushPrechecks.reduce((list: string[], func) => { - const { message } = func(markdownFile) || {}; - if (message) { - list = [...list, message]; - } - return list; - }, errors); + // errors = pushPrechecks.reduce((list: string[], func) => { + // const { message } = func(markdownFile) || {}; + // if (message) { + // list = [...list, message]; + // } + // return list; + // }, errors); if (errors.length > 0) { errors.forEach((error) => { @@ -65,8 +67,15 @@ const processPush = async (markdownFile: string) => { if (storySet.epicMap.size > 0) { console.log(chalk.yellow(`${storySet.epicMap.size} epic(s) found`)); } - const setImport = new StorySetImport(storySet, Client.factory(), configuration.clubhouse.projectId); - await setImport.create(); + if(configuration.jira.projectKey === "") { + const setImport = new ClubhouseStorySetImport(storySet, ClubhouseClient.factory(), configuration.clubhouse.projectId); + await setImport.create(); + } + else { + const setImport = new JiraStorySetImport(storySet, JiraClient.factory(), configuration.jira.projectKey || "" ) + await setImport.create(); + } + } else { chalk.red("No stories found."); } diff --git a/src/configuration.ts b/src/configuration.ts index 1bfd660..ec7bd84 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -3,6 +3,12 @@ const config = { apiToken: process.env.CLUBHOUSE_TOKEN || "", projectId: parseInt(process.env.CLUBHOUSE_PROJECT_ID || "", 10), }, + jira: { + projectKey: process.env.JIRA_PROJECT_KEY, + host: process.env.JIRA_HOST || "", + email: process.env.JIRA_EMAIL || "", + apiToken: process.env.JIRA_API_TOKEN || "" + } }; export default config; diff --git a/src/import-adapters/clubhouse/Client.ts b/src/import-adapters/clubhouse/Client.ts index ec4fe8b..b011f3e 100644 --- a/src/import-adapters/clubhouse/Client.ts +++ b/src/import-adapters/clubhouse/Client.ts @@ -12,7 +12,7 @@ type ClubhouseConfiguration = { projectId?: number; }; -class Client { +export class Client { private client: ClubhouseClient; constructor(config: ClubhouseConfiguration) { diff --git a/src/import-adapters/clubhouse/StorySetImport.ts b/src/import-adapters/clubhouse/StorySetImport.ts index 62c0f68..431f64b 100644 --- a/src/import-adapters/clubhouse/StorySetImport.ts +++ b/src/import-adapters/clubhouse/StorySetImport.ts @@ -4,7 +4,7 @@ import StorySet from "../../markdown/StorySet"; import Story from "../../markdown/Story"; import { StoryChange } from "clubhouse-lib"; -class StorySetImport { +export class StorySetImport { storySet: StorySet; projectId: number; private client: Client; diff --git a/src/import-adapters/jira/Client.ts b/src/import-adapters/jira/Client.ts new file mode 100644 index 0000000..59300c1 --- /dev/null +++ b/src/import-adapters/jira/Client.ts @@ -0,0 +1,49 @@ +import { Client as JiraClient } from "../../jira-client/Client"; +import { Issue } from "../../jira-client/Issue" +import { IssueCreationPayload } from "../../jira-client/IssueCreationPayload" +import { IssueResponse } from "../../jira-client/IssueResponse"; +import configuration from "../../configuration"; + +export class Client { + private client: Issue + + constructor(client: JiraClient) { + this.client = new Issue(client) + } + + createEpic(payload: IssueCreationPayload) : Promise { + return this.createStory({ + ...payload, + issueTypeName: "Epic" + }) + } + + async listEpics() { + return this.client.getEpics(); + } + + createStory(payload: IssueCreationPayload) : Promise { + return this.client.create(payload).then(data => data as IssueResponse).catch(err => { + console.log(err.response.body) + throw err + }); + } + + static factory(): Client { + return new Client(new JiraClient(this.makeConfiguration())); + } + + private static makeConfiguration() { + const {host, email, apiToken, projectKey} = configuration.jira + return { + host, + projectKey: projectKey || "", + authentication: { + basic: { + email, + password: apiToken + } + } + } + } +} diff --git a/src/import-adapters/jira/StorySetImport.ts b/src/import-adapters/jira/StorySetImport.ts new file mode 100644 index 0000000..12df76e --- /dev/null +++ b/src/import-adapters/jira/StorySetImport.ts @@ -0,0 +1,65 @@ +import { Client } from "./Client"; +import StorySet from "../../markdown/StorySet"; +import { IssueCreationPayload } from "../../jira-client/IssueCreationPayload"; +import Story from "../../markdown/Story" +// import Epic from "../../markdown/Epic"; + +export class StorySetImport { + storySet: StorySet; + projectKey: string; + private client: Client; + + constructor(storySet: StorySet, client: Client, projectKey: string) { + this.storySet = storySet; + this.client = client; + this.projectKey = projectKey; + } + + async create() { + const jiraList = await this.client.listEpics(); + + // build a list of persisted epics + const epicNameIdMap = new Map(); + jiraList.issues.forEach((jiraEpic) => { + epicNameIdMap.set(jiraEpic.fields.title, jiraEpic.key); + }); + + + const { epicMap, stories } = this.storySet; + + // create non-existent epics where necessary + for (const [_, epic] of epicMap) { + if (epic.name) { + const persistedEpicId = epicNameIdMap.get(epic.name); + if (!persistedEpicId) { + const persistedEpic = await this.client.createEpic({ + title: epic.name, + description: "", + projectKey: this.projectKey + }); + epicNameIdMap.set(epic.name, persistedEpic.key); + } + } + } + + // create stories + for (const story of stories) { + const storyChange = this.makeStoryChange(story); + if (story.epicName) { + storyChange.parentId = epicNameIdMap.get(story.epicName); + } + await this.client.createStory(storyChange); + } + } + + private makeStoryChange(story: Story): IssueCreationPayload { + return { + title: story.name || "Imported Story", + description: story.description, + issueTypeName: "Story", + projectKey: this.projectKey, + }; + } +} + +export default StorySetImport; diff --git a/src/jira-client/Client.ts b/src/jira-client/Client.ts new file mode 100644 index 0000000..988eda9 --- /dev/null +++ b/src/jira-client/Client.ts @@ -0,0 +1,40 @@ +import { Configuration } from "./Configuration" +import Got from "got" + + +export class Client { + private configuration: Configuration + private httpClient: typeof Got + + constructor(config: Configuration) { + this.configuration = config + this.httpClient = this.makeHttpClient() + } + + get(path, params = {}) { + return this.httpClient.get(path, { searchParams: params }) + } + + post(path, data) { + return this.httpClient.post(path, {json: { ...data, Authorization: this.buildAuthString()}}).json() + } + + private makeHttpClient() : typeof Got { + + return Got.extend({ + prefixUrl: `${this.configuration.host}/rest/api/3/`, + headers: { + "Content-Type": "application/json", + "Authorization": this.buildAuthString(), + "accept": "application/json" + }, + responseType: 'json' + }); + } + + private buildAuthString() { + const {email, password} = this.configuration.authentication.basic + const authString = Buffer.from(`${email}:${password}`).toString('base64') + return `Basic ${authString}` + } +} diff --git a/src/jira-client/ClientObject.ts b/src/jira-client/ClientObject.ts new file mode 100644 index 0000000..4b3acdc --- /dev/null +++ b/src/jira-client/ClientObject.ts @@ -0,0 +1,9 @@ +import { Client } from "./Client"; + +export abstract class ClientObject { + protected client: Client + + constructor(client: Client) { + this.client = client + } +} diff --git a/src/jira-client/Configuration.ts b/src/jira-client/Configuration.ts new file mode 100644 index 0000000..9c7f4e9 --- /dev/null +++ b/src/jira-client/Configuration.ts @@ -0,0 +1,10 @@ +export type Configuration = { + host: string, + projectKey: string, + authentication: { + basic: { + email: string, + password: string + } + } +} diff --git a/src/jira-client/Issue.ts b/src/jira-client/Issue.ts new file mode 100644 index 0000000..1aab8e0 --- /dev/null +++ b/src/jira-client/Issue.ts @@ -0,0 +1,73 @@ +import { ClientObject } from "./ClientObject"; +import { IssueCreationPayload } from "./IssueCreationPayload"; +import fnTranslate from 'md-to-adf' +import { IssueListResponse } from "./IssueListResponse"; + +interface IssueCreationPostPayload { + fields: { + description: undefined | string, + parent: undefined | { key: string }, + summary: string, + issuetype: { + name: string | undefined + } + project: { + key: string + } + } +} + +export class Issue extends ClientObject { + getList(jql) : Promise { + return this.client.get("search", {jql, validateQuery: "strict"}).json().then(data => data as IssueListResponse) + } + + getCreateMetadata() { + return this.client.get("issue/createmeta").json() + } + + getEpics() : Promise { + return this.getList("type = Epic"); + } + + + create(payload: IssueCreationPayload) { + const { title, description, projectKey, issueTypeName, parentId } = payload + let postPayload : IssueCreationPostPayload = { + fields: { + description: undefined, + parent: undefined, + summary: title, + issuetype: { + name: issueTypeName + }, + project: { + key: projectKey + } + } + } + if(description && description !== "") { + postPayload = { + ...postPayload, + fields: { + ...postPayload.fields, + description: fnTranslate(description) + } + } + } + + if(parentId && parentId !== "") { + postPayload = { + ...postPayload, + fields: { + ...postPayload.fields, + parent: { + key: parentId + } + } + + } + } + return this.client.post("issue", postPayload) + } +} diff --git a/src/jira-client/IssueCreationPayload.ts b/src/jira-client/IssueCreationPayload.ts new file mode 100644 index 0000000..5306111 --- /dev/null +++ b/src/jira-client/IssueCreationPayload.ts @@ -0,0 +1,7 @@ +export type IssueCreationPayload = { + title: string, + projectKey: string, + description?: string, + issueTypeName?: "Story" | "Bug" | "Task" | "Epic" + parentId?: string +} diff --git a/src/jira-client/IssueListResponse.ts b/src/jira-client/IssueListResponse.ts new file mode 100644 index 0000000..fb6619d --- /dev/null +++ b/src/jira-client/IssueListResponse.ts @@ -0,0 +1,7 @@ +import { IssueResponse } from "./IssueResponse" +export type IssueListResponse = { + startAt?: number, + maxResults?: number, + total?: number, + issues: IssueResponse[] +} diff --git a/src/jira-client/IssueResponse.ts b/src/jira-client/IssueResponse.ts new file mode 100644 index 0000000..50e814f --- /dev/null +++ b/src/jira-client/IssueResponse.ts @@ -0,0 +1,8 @@ +export type IssueResponse = { + id: string, + key: string, + fields: { + title: string + description?: string + } +} diff --git a/src/jira-client/IssueType.ts b/src/jira-client/IssueType.ts new file mode 100644 index 0000000..90db0f2 --- /dev/null +++ b/src/jira-client/IssueType.ts @@ -0,0 +1,7 @@ +import { ClientObject } from "./ClientObject"; + +export class IssueType extends ClientObject { + getList() { + return this.client.get("issuetype") + } +} diff --git a/src/jira-client/Project.ts b/src/jira-client/Project.ts new file mode 100644 index 0000000..9453ec4 --- /dev/null +++ b/src/jira-client/Project.ts @@ -0,0 +1,7 @@ +import { ClientObject } from "./ClientObject"; + +export class Board extends ClientObject { + getList(params = {}) { + return this.client.get("project/search", params).json() + } +} diff --git a/yarn.lock b/yarn.lock index fcdd20c..8f62b62 100644 --- a/yarn.lock +++ b/yarn.lock @@ -493,10 +493,10 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" -"@sindresorhus/is@^2.1.1": - version "2.1.1" - resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-2.1.1.tgz#ceff6a28a5b4867c2dd4a1ba513de278ccbe8bb1" - integrity sha512-/aPsuoj/1Dw/kzhkgz+ES6TxG0zfTMGLwuK2ZG00k/iJzYHTLCE8mVU8EPqEOp/lmxPoq1C1C9RYToRKb2KEfg== +"@sindresorhus/is@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.0.0.tgz#2ff674e9611b45b528896d820d3d7a812de2f0e4" + integrity sha512-FyD2meJpDPjyNQejSjvnhpgI/azsQkA4lGbuu5BQZfjvJ9cbRZXzeWL2HceCekW4lixO9JPesIIQkSoLjeJHNQ== "@sinonjs/commons@^1.7.0": version "1.8.0" @@ -695,6 +695,11 @@ acorn@^7.1.1: resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.3.1.tgz#85010754db53c3fbaf3b9ea3e083aa5c5d147ffd" integrity sha512-tLc0wSnatxAQHVHUapaHdz72pi9KUyHjq5KyHjGg9Y8Ifdc79pTh2XvI6I1/chZbnM7QtNKzh66ooDogPZSleA== +adf-builder@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/adf-builder/-/adf-builder-3.3.0.tgz#23d69266486b403727f130ae2217a262f7be78a5" + integrity sha512-zl5Sk2HtJk/E+gBnD0g51w5wIEEujaAbzsBhSsteO8Fnef7v+7pvas0FeQuLCAgOxrwy945HWAB1gQ4yjPI3/g== + ajv@^6.5.5: version "6.12.2" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.2.tgz#c629c5eced17baf314437918d2da88c99d5958cd" @@ -1705,20 +1710,19 @@ globals@^11.1.0: resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== -got@^11.3.0: - version "11.3.0" - resolved "https://registry.yarnpkg.com/got/-/got-11.3.0.tgz#25e8da8b0125b3b984613a6b719e678dd2e20406" - integrity sha512-yi/kiZY2tNMtt5IfbfX8UL3hAZWb2gZruxYZ72AY28pU5p0TZjZdl0uRsuaFbnC0JopdUi3I+Mh1F3dPQ9Dh0Q== +got@^11.8.0: + version "11.8.0" + resolved "https://registry.yarnpkg.com/got/-/got-11.8.0.tgz#be0920c3586b07fd94add3b5b27cb28f49e6545f" + integrity sha512-k9noyoIIY9EejuhaBNLyZ31D5328LeqnyPNXJQb2XlJZcKakLqN5m6O/ikhq/0lw56kUYS54fVm+D1x57YC9oQ== dependencies: - "@sindresorhus/is" "^2.1.1" + "@sindresorhus/is" "^4.0.0" "@szmarczak/http-timer" "^4.0.5" "@types/cacheable-request" "^6.0.1" "@types/responselike" "^1.0.0" cacheable-lookup "^5.0.3" cacheable-request "^7.0.1" decompress-response "^6.0.0" - get-stream "^5.1.0" - http2-wrapper "^1.0.0-beta.4.5" + http2-wrapper "^1.0.0-beta.5.2" lowercase-keys "^2.0.0" p-cancelable "^2.0.0" responselike "^2.0.0" @@ -1823,12 +1827,12 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" -http2-wrapper@^1.0.0-beta.4.5: - version "1.0.0-beta.4.6" - resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.0-beta.4.6.tgz#9438f0fceb946c8cbd365076c228a4d3bd4d0143" - integrity sha512-9oB4BiGDTI1FmIBlOF9OJ5hwJvcBEmPCqk/hy314Uhy2uq5TjekUZM8w8SPLLlUEM+mxNhXdPAXfrJN2Zbb/GQ== +http2-wrapper@^1.0.0-beta.5.2: + version "1.0.0-beta.5.2" + resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.0-beta.5.2.tgz#8b923deb90144aea65cf834b016a340fc98556f3" + integrity sha512-xYz9goEyBnC8XwXDTuC/MZ6t+MrKVQZOk4s7+PaDkwIsQd8IwqvM+0M6bA/2lvG8GHXcPdf+MejTUeO2LCPCeQ== dependencies: - quick-lru "^5.0.0" + quick-lru "^5.1.1" resolve-alpn "^1.0.0" human-signals@^1.1.1: @@ -2737,6 +2741,19 @@ markdown-table@^2.0.0: dependencies: repeat-string "^1.0.0" +marked@^0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/marked/-/marked-0.8.2.tgz#4faad28d26ede351a7a1aaa5fec67915c869e355" + integrity sha512-EGwzEeCcLniFX51DhTpmTom+dSA/MG/OBUDjnWtHbEnjAH180VzUeAw+oE4+Zv+CoYBWyRlYOTR0N8SO9R1PVw== + +md-to-adf@^0.6.4: + version "0.6.4" + resolved "https://registry.yarnpkg.com/md-to-adf/-/md-to-adf-0.6.4.tgz#c402d9a1ed570189a39d5ad200c1c88ecbdbd5e4" + integrity sha512-MynWm2w+adyzeaCJGXoOjgHAAhql0I9jR2Bq55hM7GQ05u6cGX7jThdWN6dgVSGnBmuAGmZGy2rDGBP+hJRckA== + dependencies: + adf-builder "^3.3.0" + marked "^0.8.2" + mdast-builder@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/mdast-builder/-/mdast-builder-1.1.1.tgz#ccaaa5ead8ec9c69883ec87d289770569e4b49b2" @@ -3206,7 +3223,7 @@ query-string@^6.2.0: split-on-first "^1.0.0" strict-uri-encode "^2.0.0" -quick-lru@^5.0.0: +quick-lru@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==