diff --git a/spa/src/api/subscriptions/index.ts b/spa/src/api/subscriptions/index.ts index 50e615d51..d17cfd5e1 100644 --- a/spa/src/api/subscriptions/index.ts +++ b/spa/src/api/subscriptions/index.ts @@ -1,14 +1,21 @@ import { axiosRest } from "../axiosInstance"; -import { RestSyncReqBody } from "~/src/rest-interfaces"; +import { + BackfillStatusUrlParams, + RestSyncReqBody, +} from "~/src/rest-interfaces"; export default { getSubscriptions: () => axiosRest.get("/rest/subscriptions"), + getSubscriptionsBackfillStatus: (params: BackfillStatusUrlParams) => + axiosRest.get(`/rest/subscriptions/backfill-status`, { params }), deleteGHEServer: (serverUrl: string) => axiosRest.delete(`/rest/ghes-servers/${serverUrl}`), - deleteGHEApp: (uuid: string) => - axiosRest.delete(`/rest/app/${uuid}`), + deleteGHEApp: (uuid: string) => axiosRest.delete(`/rest/app/${uuid}`), deleteSubscription: (subscriptionId: number) => axiosRest.delete(`/rest/app/cloud/subscriptions/${subscriptionId}`), syncSubscriptions: (subscriptionId: number, reqBody: RestSyncReqBody) => - axiosRest.post(`/rest/app/cloud/subscriptions/${subscriptionId}/sync`, reqBody), + axiosRest.post( + `/rest/app/cloud/subscriptions/${subscriptionId}/sync`, + reqBody + ), }; diff --git a/spa/src/pages/Connections/index.tsx b/spa/src/pages/Connections/index.tsx index 694831ff0..2e02f0727 100644 --- a/spa/src/pages/Connections/index.tsx +++ b/spa/src/pages/Connections/index.tsx @@ -11,13 +11,18 @@ import { GHSubscriptions, BackfillPageModalTypes, SuccessfulConnection, - GitHubEnterpriseApplication, + GitHubEnterpriseApplication } from "../../rest-interfaces"; import SkeletonForLoading from "./SkeletonForLoading"; import SubscriptionManager from "../../services/subscription-manager"; import RestartBackfillModal from "./Modals/RestartBackfillModal"; import DisconnectSubscriptionModal from "./Modals/DisconnectSubscriptionModal"; -import { DisconnectGHEServerModal, DeleteAppInGitHubModal, DisconnectGHEServerAppModal } from "./Modals/DisconnectGHEServerModal"; +import { + DisconnectGHEServerModal, + DeleteAppInGitHubModal, + DisconnectGHEServerAppModal, +} from "./Modals/DisconnectGHEServerModal"; +import { getInProgressSubIds, getUpdatedSubscriptions } from "../../utils"; const hasGHCloudConnections = (subscriptions: GHSubscriptions): boolean => subscriptions?.ghCloudSubscriptions && @@ -33,6 +38,13 @@ const Connections = () => { const [dataForModal, setDataForModal] = useState< SuccessfulConnection | GitHubEnterpriseApplication | undefined >(undefined); + const [isLoading, setIsLoading] = useState(false); + const [subscriptions, setSubscriptions] = useState( + null + ); + const [inProgressSubs, setInProgressSubs] = useState | null>( + null + ); const openedModal = () => { switch (selectedModal) { case "BACKFILL": @@ -80,11 +92,6 @@ const Connections = () => { } }; - const [isLoading, setIsLoading] = useState(false); - const [subscriptions, setSubscriptions] = useState( - null - ); - const fetchGHSubscriptions = async () => { try { setIsLoading(true); @@ -92,8 +99,13 @@ const Connections = () => { if (response instanceof AxiosError) { // TODO: Handle the error once we have the designs console.error("Error", response); + } else { + const inProgressSubIds = getInProgressSubIds(response); + if (inProgressSubIds && inProgressSubIds.length > 0) { + setInProgressSubs(inProgressSubIds); + } + setSubscriptions(response as GHSubscriptions); } - setSubscriptions(response as GHSubscriptions); } catch (e) { // TODO: handle this error in UI/Modal ? console.error("Could not fetch ghe subscriptions: ", e); @@ -102,6 +114,39 @@ const Connections = () => { } }; + const fetchBackfillStatus = async (inProgressSubs: Array) => { + try { + const response = await SubscriptionManager.getSubscriptionsBackfillStatus( + inProgressSubs.toString() + ); + if (response instanceof AxiosError) { + // TODO: Handle the error once we have the designs + console.error("Error", response); + } else { + if(subscriptions){ + const newSubscriptions = getUpdatedSubscriptions(response, subscriptions); + if(newSubscriptions) { + setSubscriptions(newSubscriptions); + } + } + if (!response.isBackfillComplete) { + setTimeout(() => { + fetchBackfillStatus(inProgressSubs); + }, 3000); + } + } + } catch (e) { + // TODO: handle this error in UI/Modal ? + console.error("Could not fetch ghe subscriptions: ", e); + } + }; + + useEffect(() => { + if (inProgressSubs && inProgressSubs.length > 0) { + fetchBackfillStatus(inProgressSubs); + } + }, [inProgressSubs]); + useEffect(() => { fetchGHSubscriptions(); }, []); diff --git a/spa/src/services/subscription-manager/index.ts b/spa/src/services/subscription-manager/index.ts index 01e9221b3..eeadcb755 100644 --- a/spa/src/services/subscription-manager/index.ts +++ b/spa/src/services/subscription-manager/index.ts @@ -2,7 +2,7 @@ import Api from "../../api"; import { AxiosError } from "axios"; import { reportError } from "../../utils"; import { GHSubscriptions } from "../../../../src/rest-interfaces"; -import { RestSyncReqBody } from "~/src/rest-interfaces"; +import { BackfillStatusResp, RestSyncReqBody } from "~/src/rest-interfaces"; async function syncSubscription(subscriptionId:number, reqBody: RestSyncReqBody): Promise { try { @@ -26,14 +26,32 @@ async function getSubscriptions(): Promise { const isSuccessful = response.status === 200; if(!isSuccessful) { reportError( - { message: "Response status for getting subscriptions is not 204", status: response.status }, + { message: "Response status for getting subscriptions is not 200", status: response.status }, { path: "getSubscriptions" } ); } return response.data; } catch (e: unknown) { - reportError(new Error("Unable to delete subscription", { cause: e }), { path: "getSubscriptions" }); + reportError(new Error("Unable to get subscription", { cause: e }), { path: "getSubscriptions" }); + return e as AxiosError; + } +} + +async function getSubscriptionsBackfillStatus(subscriptionIds: string): Promise { + try { + const response= await Api.subscriptions.getSubscriptionsBackfillStatus({ subscriptionIds }); + const isSuccessful = response.status === 200; + if(!isSuccessful) { + reportError( + { message: "Response status for getting subscriptions backfill status is not 200", status: response.status }, + { path: "getSubscriptionsBackfillStatus" } + ); + } + + return response.data; + } catch (e: unknown) { + reportError(new Error("Unable to Get subscription backfill status update", { cause: e }), { path: "getSubscriptionsBackfillStatus" }); return e as AxiosError; } } @@ -92,6 +110,7 @@ async function deleteGHEApp(uuid: string): Promise { } export default { getSubscriptions, + getSubscriptionsBackfillStatus, deleteSubscription, deleteGHEServer, syncSubscription, diff --git a/spa/src/utils/index.ts b/spa/src/utils/index.ts index 0d4254022..50c12e3b9 100644 --- a/spa/src/utils/index.ts +++ b/spa/src/utils/index.ts @@ -1,5 +1,6 @@ import * as Sentry from "@sentry/react"; import { AxiosError } from "axios"; +import { BackfillStatusResp, GHSubscriptions, SubscriptionBackfillState } from "../rest-interfaces"; export const getJiraJWT = (): Promise => new Promise(resolve => { return AP.context.getToken((token: string) => { @@ -22,7 +23,7 @@ export function reportError(err: unknown, extra: { try { const cause = (err as Record).cause || {}; - delete (err as Record).cause; //so that Sentry doesn't group all axios error together + delete (err as Record).cause; //so that Sentry doesn"t group all axios error together Sentry.captureException(err, { extra: { @@ -58,3 +59,160 @@ export function openChildWindow(url: string) { }, 100); return child; } + +export const getInProgressSubIds = (response: GHSubscriptions): Array => { + const successfulCloudConnections = + response.ghCloudSubscriptions.successfulCloudConnections; + const inProgressCloudConnections = successfulCloudConnections.filter( + (connection) => + connection.syncStatus === "IN PROGRESS" || + connection.syncStatus === "PENDING" + ); + const inProgressCloudSubIds = inProgressCloudConnections.map( + (connection) => connection.subscriptionId + ); + let inProgressGHESubIds: Array = []; + const ghEnterpriseServers = response.ghEnterpriseServers; + for (const ghEnterpriseServer of ghEnterpriseServers) { + const applications = ghEnterpriseServer.applications; + for (const application of applications) { + const successfulGHEConnections = application.successfulConnections; + const inProgressGHEConnections = successfulGHEConnections.filter( + (connection) => + connection.syncStatus === "IN PROGRESS" || + connection.syncStatus === "PENDING" + ); + inProgressGHESubIds = inProgressGHEConnections.map( + (connection) => connection.subscriptionId + ); + } + } + return [...inProgressCloudSubIds, ...inProgressGHESubIds]; +}; + +const getUpdatedCloudSubs = ( + currentSubs: GHSubscriptions, + subscriptionId: number, + subscription: SubscriptionBackfillState +) => { + let matchedIndex; + const successfulCloudConnections = + currentSubs?.ghCloudSubscriptions.successfulCloudConnections; + if (successfulCloudConnections) { + matchedIndex = successfulCloudConnections.findIndex( + (connection) => connection.subscriptionId === subscriptionId + ); + successfulCloudConnections[matchedIndex] = { + ...successfulCloudConnections[matchedIndex], + numberOfSyncedRepos: subscription.syncedRepos, + syncStatus: subscription.syncStatus, + }; + if (subscription.backfillSince) { + successfulCloudConnections[matchedIndex]["backfillSince"] = + subscription.backfillSince; + } + return { + ...currentSubs, + ghCloudSubscriptions: { + ...currentSubs.ghCloudSubscriptions, + successfulCloudConnections: successfulCloudConnections, + }, + }; + } +}; + +const getUpdatedGHESubs = ( + currentSubs: GHSubscriptions, + subscription: SubscriptionBackfillState +) => { + const ghEnterpriseServers = currentSubs.ghEnterpriseServers; + let ghEnterpriseServerIndex; + let applicationIndex; + for (const [ + ghEnterpriseServerI, + ghEnterpriseServer, + ] of ghEnterpriseServers.entries()) { + const applications = ghEnterpriseServer.applications; + + for (const [appIndex, app] of applications.entries()) { + if (app.id === subscription.gitHubAppId) { + ghEnterpriseServerIndex = ghEnterpriseServerI; + applicationIndex = appIndex; + break; + } + } + } + if ( + typeof ghEnterpriseServerIndex === "number" && + !isNaN(ghEnterpriseServerIndex) && + typeof applicationIndex === "number" && + !isNaN(applicationIndex) + ) { + const newGHEnterpriseServers = ghEnterpriseServers; + const apps = ghEnterpriseServers[ghEnterpriseServerIndex].applications; + const newApps = [...apps]; + + if (subscription.gitHubAppId) { + const successfulConnections = + newApps[applicationIndex]?.successfulConnections; + const newSuccessfulConnections = successfulConnections.map( + (connection) => { + if (connection.subscriptionId === subscription.id) { + const result = { + ...connection, + syncStatus: subscription.syncStatus, + numberOfSyncedRepos: subscription.syncedRepos, + + }; + if (subscription.backfillSince) { + result["backfillSince"] = + subscription.backfillSince; + } + return result; + } + return connection; + } + ); + + newApps[applicationIndex] = { + ...newApps[applicationIndex], + successfulConnections: [...newSuccessfulConnections], + }; + } + + newGHEnterpriseServers[ghEnterpriseServerIndex] = { + ...ghEnterpriseServers[ghEnterpriseServerIndex], + applications: newApps, + }; + const result = { + ...currentSubs, + ghEnterpriseServers: newGHEnterpriseServers, + }; + return result; + } +}; + +export const getUpdatedSubscriptions = ( + response: BackfillStatusResp, + subscriptions: GHSubscriptions +): GHSubscriptions | undefined => { + const currentSubs = subscriptions; + let newSubs; + const subscriptionIds = response.subscriptionIds || []; + if (currentSubs) { + for (const subscriptionId of subscriptionIds) { + const subscriptions = response.subscriptions; + const subscription = subscriptions[subscriptionId]; + if (!subscription.gitHubAppId) { + newSubs = getUpdatedCloudSubs( + currentSubs, + subscriptionId, + subscription + ); + } else { + newSubs = getUpdatedGHESubs(currentSubs, subscription); + } + } + return newSubs; + } +}; diff --git a/src/rest-interfaces/index.ts b/src/rest-interfaces/index.ts index 03aedd0dc..cf3814cd4 100644 --- a/src/rest-interfaces/index.ts +++ b/src/rest-interfaces/index.ts @@ -24,6 +24,10 @@ export type DeferredInstallationUrlParams = { gitHubOrgName: string; }; +export type BackfillStatusUrlParams = { + subscriptionIds: string; +}; + export type DeferralParsedRequest = { orgName: string; jiraHost: string; @@ -187,3 +191,32 @@ export type GHSubscriptions = { }; export type BackfillPageModalTypes = "BACKFILL" | "DISCONNECT_SUBSCRIPTION" | "DISCONNECT_SERVER_APP" | "DISCONNECT_SERVER" | "DELETE_GHE_APP"; + +export type ConnectionSyncStatus = "IN PROGRESS" | "FINISHED" | "PENDING" | "FAILED"; + +export type SubscriptionBackfillState = { + id: number; + totalRepos?: number; + syncedRepos: number; + syncStatus: ConnectionSyncStatus; + isSyncComplete: boolean; + backfillSince?: string; + failedSyncErrors?: Record; + syncWarning?: string; + gitHubAppId?: number; +}; + +export type BackfillStatusError = { + subscriptionId: string; + error: string; +}; +export type BackFillType = { + [key: string]: SubscriptionBackfillState; +}; + +export type BackfillStatusResp = { + subscriptions: BackFillType; + isBackfillComplete: boolean; + subscriptionIds: Array; + errors: BackfillStatusError; +}; diff --git a/src/rest/routes/subscriptions/backfill-status.ts b/src/rest/routes/subscriptions/backfill-status.ts new file mode 100644 index 000000000..029780481 --- /dev/null +++ b/src/rest/routes/subscriptions/backfill-status.ts @@ -0,0 +1,117 @@ +import { Request, Response } from "express"; +import { groupBy } from "lodash"; +import { RepoSyncState } from "~/src/models/reposyncstate"; +import { Subscription, SyncStatus } from "~/src/models/subscription"; +import { + mapSyncStatus, + getRetryableFailedSyncErrors +} from "~/src/util/github-installations-helper"; +import { errorWrapper } from "../../helper"; +import { + BackFillType, + SubscriptionBackfillState, + BackfillStatusError +} from "../../../../spa/src/rest-interfaces"; +import { BaseLocals } from ".."; +import { InsufficientPermissionError, InvalidArgumentError } from "~/src/config/errors"; + +const GetSubscriptionsBackfillStatus = async (req: Request, res: Response) => { + const { jiraHost: localJiraHost } = res.locals; + const { subscriptionIds } = req.query; + + const subIds = String(subscriptionIds) + .split(",") + .map(Number) + .filter(Boolean); + if (subIds.length === 0) { + req.log.warn("Missing Subscription IDs"); + throw new InvalidArgumentError("Missing Subscription IDs"); + } + + const subscriptions = await Subscription.findAllForSubscriptionIds( + subIds + ); + + const resultSubscriptionIds: Array = subscriptions.map( + (subscription) => subscription.id + ); + + if (subscriptions.length === 0) { + req.log.error("Missing Subscription"); + throw new InvalidArgumentError("Missing Subscription"); + } + + const jiraHostsMatched = subscriptions.every( + (subscription) => subscription.jiraHost === localJiraHost + ); + + if (!jiraHostsMatched) { + req.log.error("mismatched Jira Host"); + throw new InsufficientPermissionError("mismatched Jira Host"); + } + const subscriptionsById = groupBy(subscriptions, "id"); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const { backfillStatus, errors } = await getBackfillStatus( + subscriptionsById + ); + const isBackfillComplete = getBackfillCompletionStatus(backfillStatus); + res.status(200).send({ + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + subscriptions: backfillStatus, + isBackfillComplete, + subscriptionIds: resultSubscriptionIds, + errors + }); +}; + +const getBackfillCompletionStatus = (backfillStatus: BackFillType): boolean => + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + Object.values(backfillStatus).every( + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + (backFill: SubscriptionBackfillState): boolean => backFill.isSyncComplete + ); + +const getBackfillStatus = async ( + subscriptionsById +): Promise<{ + backfillStatus: BackFillType; + errors?: BackfillStatusError[]; +}> => { + const backfillStatus: BackFillType = {}; + const errors: BackfillStatusError[] = []; + for (const subscriptionId in subscriptionsById) { + try { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access + const subscription: Subscription = subscriptionsById[subscriptionId][0]; + const isSyncComplete = + subscription.syncStatus === SyncStatus.COMPLETE || + subscription.syncStatus === SyncStatus.FAILED; + const failedSyncErrors = await getRetryableFailedSyncErrors(subscription); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + backfillStatus[subscriptionId] = { + id: parseInt(subscriptionId), + isSyncComplete, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + syncStatus: mapSyncStatus(subscription.syncStatus), + totalRepos: subscription.totalNumberOfRepos, + syncedRepos: await RepoSyncState.countFullySyncedReposForSubscription( + subscription + ), + failedSyncErrors, + backfillSince: subscription.backfillSince?.toString(), + syncWarning: subscription.syncWarning, + gitHubAppId: subscription.gitHubAppId + }; + } catch (error: unknown) { + errors.push({ subscriptionId, error: JSON.stringify(error) }); + } + } + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + return { backfillStatus, errors }; +}; + +export const GetSubBackfillStatusHandler = errorWrapper( + "GetSubBackfillStatusHandler", + GetSubscriptionsBackfillStatus +); diff --git a/src/rest/routes/subscriptions/index.ts b/src/rest/routes/subscriptions/index.ts index da165809c..5833cb0e2 100644 --- a/src/rest/routes/subscriptions/index.ts +++ b/src/rest/routes/subscriptions/index.ts @@ -6,6 +6,7 @@ import { removeSubscription } from "utils/jira-utils"; import { GitHubServerApp } from "models/github-server-app"; import { InvalidArgumentError } from "config/errors"; import { SyncRouterHandler } from "./sync"; +import { GetSubBackfillStatusHandler } from "./backfill-status"; export const SubscriptionsRouter = Router({ mergeParams: true }); @@ -19,6 +20,8 @@ SubscriptionsRouter.get("/", errorWrapper("SubscriptionsGet", async (req: Reques }); })); +SubscriptionsRouter.get("/backfill-status", GetSubBackfillStatusHandler); + /** * This delete endpoint only handles Github cloud subscriptions */ diff --git a/src/util/github-installations-helper.ts b/src/util/github-installations-helper.ts index 59b1ed959..b5ec188dd 100644 --- a/src/util/github-installations-helper.ts +++ b/src/util/github-installations-helper.ts @@ -8,6 +8,7 @@ import { RepoSyncState } from "models/reposyncstate"; import { statsd } from "config/statsd"; import { metricError } from "config/metric-names"; import { groupBy, countBy } from "lodash"; +import { ConnectionSyncStatus } from "~/spa/src/rest-interfaces"; interface FailedConnection { id: number; @@ -30,8 +31,7 @@ interface GitHubCloudObj { failedConnections: FailedConnection[] } -export type ConnectionSyncStatus = "IN PROGRESS" | "FINISHED" | "PENDING" | "FAILED" | undefined; -const mapSyncStatus = (syncStatus: SyncStatus = SyncStatus.PENDING): ConnectionSyncStatus => { +export const mapSyncStatus = (syncStatus: SyncStatus = SyncStatus.PENDING): ConnectionSyncStatus => { switch (syncStatus) { case "ACTIVE": return "IN PROGRESS"; @@ -76,6 +76,7 @@ const getInstallation = async (subscription: Subscription, gitHubAppId: number | return { ...response.data, subscriptionId: subscription.id, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment syncStatus: mapSyncStatus(subscription.syncStatus), syncWarning: subscription.syncWarning, totalNumberOfRepos: subscription.totalNumberOfRepos, diff --git a/src/util/handlebars/handlebar-helpers.ts b/src/util/handlebars/handlebar-helpers.ts index ba733a224..546fb6063 100644 --- a/src/util/handlebars/handlebar-helpers.ts +++ b/src/util/handlebars/handlebar-helpers.ts @@ -1,6 +1,6 @@ import hbs from "hbs"; import { isPlainObject } from "lodash"; -import { ConnectionSyncStatus } from "utils/github-installations-helper"; +import { ConnectionSyncStatus } from "~/spa/src/rest-interfaces"; export const concatStringHelper = (...strings: string[]) => strings.filter((arg: unknown) => typeof arg !== "object").join(" "); // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition diff --git a/test/snapshots/app.test.ts.snap b/test/snapshots/app.test.ts.snap index c81a6a48a..95d6fe285 100644 --- a/test/snapshots/app.test.ts.snap +++ b/test/snapshots/app.test.ts.snap @@ -15,6 +15,8 @@ exports[`app getFrontendApp please review routes and update snapshot when adding query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,serveStatic :GET ^/?(?=/|$)^/rest/?(?=/|$)^/subscriptions/?(?=/|$)^/?$ query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,SubscriptionsGet +:GET ^/?(?=/|$)^/rest/?(?=/|$)^/subscriptions/?(?=/|$)^/backfill-status/?$ + query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,GetSubBackfillStatusHandler :DELETE ^/?(?=/|$)^/rest/?(?=/|$)^/subscriptions/?(?=/|$)^/?$ query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,SubscriptionDelete :POST ^/?(?=/|$)^/rest/?(?=/|$)^/subscriptions/?(?=/|$)^/sync/?$ @@ -51,6 +53,8 @@ exports[`app getFrontendApp please review routes and update snapshot when adding query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,tempReplaceUUID,GithubServerAppMiddleware,JiraCloudIDGet :GET ^/?(?=/|$)^/rest/?(?=/|$)^/app/(?:([^/]+?))/?(?=/|$)^/subscriptions/(?:([^/]+?))/?(?=/|$)^/?$ query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,tempReplaceUUID,GithubServerAppMiddleware,SubscriptionsGet +:GET ^/?(?=/|$)^/rest/?(?=/|$)^/app/(?:([^/]+?))/?(?=/|$)^/subscriptions/(?:([^/]+?))/?(?=/|$)^/backfill-status/?$ + query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,tempReplaceUUID,GithubServerAppMiddleware,GetSubBackfillStatusHandler :DELETE ^/?(?=/|$)^/rest/?(?=/|$)^/app/(?:([^/]+?))/?(?=/|$)^/subscriptions/(?:([^/]+?))/?(?=/|$)^/?$ query,expressInit,elapsedTimeMetrics,sentryRequestMiddleware,urlencodedParser,jsonParser,cookieParser,LogMiddleware,JwtHandler,jiraAdminEnforceMiddleware,tempReplaceUUID,GithubServerAppMiddleware,SubscriptionDelete :POST ^/?(?=/|$)^/rest/?(?=/|$)^/app/(?:([^/]+?))/?(?=/|$)^/subscriptions/(?:([^/]+?))/?(?=/|$)^/sync/?$