app/bugzilla.mjs (160 lines of code) (raw):

import * as Global from "global"; export function queryURL(query, components, includeFields) { const search = new URLSearchParams(); query.query_format = "advanced"; // add provided query parameters let fieldNumber = 0; for (const [name, value] of Object.entries(query)) { if (name[0] === "f") { if (name === "j_top") { throw "Cannot set j_top, use a group with an OR joiner (f#=OP,j#=OR)"; } const num = name.slice(1) * 1; if (num > fieldNumber) { fieldNumber = num; } search.append(name, value); } else { if (Array.isArray(value)) { for (const v of value) { search.append(name, v); } } else { search.append(name, value); } } } // Add components. We can't use product= and component= query parameters as it // hits on matching products OR components, rather than a product/component pair. // Instead we build a query which does: // .. ((product AND component) OR (product AND component) ...) fieldNumber++; search.append("f" + fieldNumber, "OP"); search.append("j" + fieldNumber, "OR"); if (components) { for (const c of components) { fieldNumber++; search.append("f" + fieldNumber, "OP"); fieldNumber++; search.append("f" + fieldNumber, "product"); search.append("o" + fieldNumber, "equals"); search.append("v" + fieldNumber, c.product); fieldNumber++; search.append("f" + fieldNumber, "component"); search.append("o" + fieldNumber, "equals"); search.append("v" + fieldNumber, c.component); fieldNumber++; search.append("f" + fieldNumber, "CP"); } } fieldNumber++; search.append("f" + fieldNumber, "CP"); search.append( "include_fields", includeFields || [ "assigned_to", "component", "creation_time", "creator", "flags", "groups", "id", "keywords", "last_change_time", "priority", "product", "severity", "summary", "triage_owner", "type", ].join(","), ); search.append("limit", "0"); return "bug?" + search.toString(); } export function queryUrlToBuglistUrl(queryUrl, additional) { // take a request endpoint url as generated by queryURL and return a full qualified buglist.cgi url const params = new URLSearchParams(queryUrl.replace(/^bug\?/, "")); params.delete("include_fields"); const nameMap = { // eslint-disable-next-line camelcase include_fields: "", // eslint-disable-next-line camelcase count_only: "", limit: "", severity: "bug_severity", type: "bug_type", }; for (const [name, newName] of Object.entries(nameMap)) { if (params.has(name)) { if (newName) { params.set(newName, params.get(name)); } params.delete(name); } } if (additional) { for (const [name, value] of Object.entries(additional)) { params.set(name, value); } } return `https://bugzilla.mozilla.org/buglist.cgi?${params.toString()}`; } export function buglistUrl(ids) { return ( "https://bugzilla.mozilla.org/buglist.cgi?order=bug_list&bug_id=" + ids.join(",") ); } export function setApiKey(key) { window.localStorage.setItem("api-key", key); } export function getApiKey() { return window.localStorage.getItem("api-key") || ""; } export async function whoami() { const response = await rest("whoami", undefined, true); return response.error && response.code === 306 ? undefined : response; } export async function rest(endpoint, args, ignoreErrors, textResponse) { let url = `https://bugzilla.mozilla.org/rest/${endpoint}`; if (args) { if (args instanceof Object) { args = new URLSearchParams(args).toString(); } url = url + "?" + args; } const apiKey = getApiKey(); const account = Global.getAccount(); const uaSuffix = account ? ` (${account.name})` : ""; const controller = new AbortController(); const timerId = setTimeout(() => controller.abort(), 60000); let response; try { response = await fetch(url, { method: "GET", headers: { "User-Agent": `bugdash${uaSuffix}`, "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "cross-site", "X-Bugzilla-API-Key": apiKey, }, signal: controller.signal, }); } catch (error) { clearTimeout(timerId); controller.abort(); throw new Error(error.message); } const responseData = textResponse ? await response.text() : await response.json(); clearTimeout(timerId); if (!response.ok && !responseData) { throw new Error(response.statusText); } if (responseData.error && !ignoreErrors) { throw new Error(responseData.message); } return responseData; }