Просмотр исходного кода

Merge pull request #4608 from weseek/feat/api-get-pages-around-target

feat: Api get pages around target
Haku Mizuki 4 лет назад
Родитель
Сommit
b697845d02

+ 2 - 1
packages/app/package.json

@@ -59,8 +59,8 @@
     "@google-cloud/storage": "^5.8.5",
     "@google-cloud/storage": "^5.8.5",
     "@growi/codemirror-textlint": "^4.4.10-RC.0",
     "@growi/codemirror-textlint": "^4.4.10-RC.0",
     "@growi/plugin-attachment-refs": "^4.4.10-RC.0",
     "@growi/plugin-attachment-refs": "^4.4.10-RC.0",
-    "@growi/plugin-pukiwiki-like-linker": "^4.4.10-RC.0",
     "@growi/plugin-lsx": "^4.4.10-RC.0",
     "@growi/plugin-lsx": "^4.4.10-RC.0",
+    "@growi/plugin-pukiwiki-like-linker": "^4.4.10-RC.0",
     "@growi/slack": "^4.4.10-RC.0",
     "@growi/slack": "^4.4.10-RC.0",
     "@promster/express": "^5.1.0",
     "@promster/express": "^5.1.0",
     "@promster/server": "^6.0.3",
     "@promster/server": "^6.0.3",
@@ -130,6 +130,7 @@
     "passport-saml": "^2.2.0",
     "passport-saml": "^2.2.0",
     "passport-twitter": "^1.0.4",
     "passport-twitter": "^1.0.4",
     "prom-client": "^13.0.0",
     "prom-client": "^13.0.0",
+    "re2": "^1.16.0",
     "react-card-flip": "^1.0.10",
     "react-card-flip": "^1.0.10",
     "react-image-crop": "^8.3.0",
     "react-image-crop": "^8.3.0",
     "reconnecting-websocket": "^4.4.0",
     "reconnecting-websocket": "^4.4.0",

+ 7 - 0
packages/app/src/interfaces/common.ts

@@ -0,0 +1,7 @@
+/*
+ * Common types and interfaces
+ */
+
+
+// Foreign key field
+export type Ref<T> = string | T;

+ 21 - 4
packages/app/src/interfaces/page.ts

@@ -1,14 +1,31 @@
+import { Ref } from './common';
 import { IUser } from './user';
 import { IUser } from './user';
 import { IRevision } from './revision';
 import { IRevision } from './revision';
 import { ITag } from './tag';
 import { ITag } from './tag';
 
 
+
 export type IPage = {
 export type IPage = {
   path: string,
   path: string,
   status: string,
   status: string,
-  revision: IRevision,
-  tags: ITag[],
-  creator: IUser,
+  revision: Ref<IRevision>,
+  tags: Ref<ITag>[],
+  creator: Ref<IUser>,
   createdAt: Date,
   createdAt: Date,
   updatedAt: Date,
   updatedAt: Date,
-  seenUsers: string[]
+  seenUsers: Ref<IUser>[],
+  parent: Ref<IPage>,
+  isEmpty: boolean,
+  redirectTo: string,
+  grant: number,
+  grantedUsers: Ref<IUser>[],
+  grantedGroup: Ref<any>,
+  lastUpdateUser: Ref<IUser>,
+  liker: Ref<IUser>[],
+  commentCount: number
+  slackChannels: string,
+  pageIdOnHackmd: string,
+  revisionHackmdSynced: Ref<IRevision>,
+  hasDraftOnHackmd: boolean,
+  deleteUser: Ref<IUser>,
+  deletedAt: Date,
 }
 }

+ 9 - 0
packages/app/src/server/models/obsolete-page.js

@@ -219,6 +219,15 @@ export class PageQueryBuilder {
     return this;
     return this;
   }
   }
 
 
+  /*
+   * Add this condition when get any ancestor pages including the target's parent
+   */
+  addConditionToSortAncestorPages() {
+    this.query = this.query.sort('-path');
+
+    return this;
+  }
+
   addConditionToListByPathsArray(paths) {
   addConditionToListByPathsArray(paths) {
     this.query = this.query
     this.query = this.query
       .and({
       .and({

+ 98 - 12
packages/app/src/server/models/page.ts

@@ -6,6 +6,7 @@ import mongoose, {
 import mongoosePaginate from 'mongoose-paginate-v2';
 import mongoosePaginate from 'mongoose-paginate-v2';
 import uniqueValidator from 'mongoose-unique-validator';
 import uniqueValidator from 'mongoose-unique-validator';
 import nodePath from 'path';
 import nodePath from 'path';
+import RE2 from 're2';
 
 
 import { getOrCreateModel, pagePathUtils } from '@growi/core';
 import { getOrCreateModel, pagePathUtils } from '@growi/core';
 import loggerFactory from '../../utils/logger';
 import loggerFactory from '../../utils/logger';
@@ -35,6 +36,9 @@ export interface PageDocument extends IPage, Document {}
 export interface PageModel extends Model<PageDocument> {
 export interface PageModel extends Model<PageDocument> {
   createEmptyPagesByPaths(paths: string[]): Promise<void>
   createEmptyPagesByPaths(paths: string[]): Promise<void>
   getParentIdAndFillAncestors(path: string): Promise<string | null>
   getParentIdAndFillAncestors(path: string): Promise<string | null>
+  findByPathAndViewer(path: string | null, user, userGroups?, useFindOne?): Promise<PageDocument[]>
+  findSiblingsByPathAndViewer(path: string | null, user, userGroups?): Promise<PageDocument[]>
+  findAncestorsByPathOrId(pathOrId: string): Promise<PageDocument[]>
 }
 }
 
 
 const ObjectId = mongoose.Schema.Types.ObjectId;
 const ObjectId = mongoose.Schema.Types.ObjectId;
@@ -79,14 +83,20 @@ schema.plugin(uniqueValidator);
  * Methods
  * Methods
  */
  */
 const collectAncestorPaths = (path: string, ancestorPaths: string[] = []): string[] => {
 const collectAncestorPaths = (path: string, ancestorPaths: string[] = []): string[] => {
+  if (isTopPage(path)) return ancestorPaths;
+
   const parentPath = nodePath.dirname(path);
   const parentPath = nodePath.dirname(path);
   ancestorPaths.push(parentPath);
   ancestorPaths.push(parentPath);
+  return collectAncestorPaths(parentPath, ancestorPaths);
+};
 
 
-  if (!isTopPage(path)) return collectAncestorPaths(parentPath, ancestorPaths);
-
-  return ancestorPaths;
+const hasSlash = (str: string): boolean => {
+  return str.includes('/');
 };
 };
 
 
+/*
+ * Create empty pages if the page in paths didn't exist
+ */
 schema.statics.createEmptyPagesByPaths = async function(paths: string[]): Promise<void> {
 schema.statics.createEmptyPagesByPaths = async function(paths: string[]): Promise<void> {
   // find existing parents
   // find existing parents
   const builder = new PageQueryBuilder(this.find({}, { _id: 0, path: 1 }));
   const builder = new PageQueryBuilder(this.find({}, { _id: 0, path: 1 }));
@@ -110,7 +120,14 @@ schema.statics.createEmptyPagesByPaths = async function(paths: string[]): Promis
   }
   }
 };
 };
 
 
-schema.statics.getParentIdAndFillAncestors = async function(path: string): Promise<string | null> {
+/*
+ * Find the pages parent and update if the parent exists.
+ * If not,
+ *   - first   run createEmptyPagesByPaths with ancestor's paths to ensure all the ancestors exist
+ *   - second  update ancestor pages' parent
+ *   - finally return the target's parent page id
+ */
+schema.statics.getParentIdAndFillAncestors = async function(path: string): Promise<Schema.Types.ObjectId> {
   const parentPath = nodePath.dirname(path);
   const parentPath = nodePath.dirname(path);
 
 
   const parent = await this.findOne({ path: parentPath }); // find the oldest parent which must always be the true parent
   const parent = await this.findOne({ path: parentPath }); // find the oldest parent which must always be the true parent
@@ -127,6 +144,7 @@ schema.statics.getParentIdAndFillAncestors = async function(path: string): Promi
   const builder = new PageQueryBuilder(this.find({}, { _id: 1, path: 1 }));
   const builder = new PageQueryBuilder(this.find({}, { _id: 1, path: 1 }));
   const ancestors = await builder
   const ancestors = await builder
     .addConditionToListByPathsArray(ancestorPaths)
     .addConditionToListByPathsArray(ancestorPaths)
+    .addConditionToSortAncestorPages()
     .query
     .query
     .lean()
     .lean()
     .exec();
     .exec();
@@ -157,25 +175,93 @@ schema.statics.getParentIdAndFillAncestors = async function(path: string): Promi
   return parentId;
   return parentId;
 };
 };
 
 
-schema.statics.findByPathAndViewer = async function(path: string | null, user, userGroups, useFindOne = true): Promise<IPage[]> {
-  if (path == null) {
-    throw new Error('path is required.');
-  }
-
-  const baseQuery = useFindOne ? this.findOne({ path }) : this.find({ path });
-
+// Utility function to add viewer condition to PageQueryBuilder instance
+const addViewerCondition = async(queryBuilder: PageQueryBuilder, user, userGroups = null): Promise<void> => {
   let relatedUserGroups = userGroups;
   let relatedUserGroups = userGroups;
   if (user != null && relatedUserGroups == null) {
   if (user != null && relatedUserGroups == null) {
     const UserGroupRelation: any = mongoose.model('UserGroupRelation');
     const UserGroupRelation: any = mongoose.model('UserGroupRelation');
     relatedUserGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
     relatedUserGroups = await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user);
   }
   }
 
 
-  const queryBuilder = new PageQueryBuilder(baseQuery);
   queryBuilder.addConditionToFilteringByViewer(user, relatedUserGroups, true);
   queryBuilder.addConditionToFilteringByViewer(user, relatedUserGroups, true);
+};
+
+/*
+ * Find a page by path and viewer. Pass false to useFindOne to use findOne method.
+ */
+schema.statics.findByPathAndViewer = async function(
+    path: string | null, user, userGroups = null, useFindOne = true,
+): Promise<PageDocument | PageDocument[] | null> {
+  if (path == null) {
+    throw new Error('path is required.');
+  }
+
+  const baseQuery = useFindOne ? this.findOne({ path }) : this.find({ path });
+  const queryBuilder = new PageQueryBuilder(baseQuery);
+  await addViewerCondition(queryBuilder, user, userGroups);
 
 
   return queryBuilder.query.exec();
   return queryBuilder.query.exec();
 };
 };
 
 
+/*
+ * Find the siblings including the target page. When top page, it returns /any_level1 pages
+ */
+schema.statics.findSiblingsByPathAndViewer = async function(path: string | null, user, userGroups): Promise<PageDocument[]> {
+  if (path == null) {
+    throw new Error('path is required.');
+  }
+
+  const _parentPath = nodePath.dirname(path);
+  const parentPath = isTopPage(_parentPath) ? '' : _parentPath;
+
+  // https://regex101.com/r/mrDJrx/1
+  // ex. /parent/any_child OR /any_level1
+  let regexp = new RE2(`^${parentPath}(\\/[^/]+)\\/?$`);
+  // https://regex101.com/r/iu1vYF/1
+  // ex. / OR /any_level1
+  if (isTopPage(path)) regexp = new RE2(/^\/[^\\/]*$/);
+
+  const queryBuilder = new PageQueryBuilder(this.find({ path: { $regex: regexp.source, $options: regexp.flags } }));
+  await addViewerCondition(queryBuilder, user, userGroups);
+
+  return queryBuilder.query.lean().exec();
+};
+
+/*
+ * Find all ancestor pages by path. When duplicate pages found, it uses the oldest page as a result
+ */
+schema.statics.findAncestorsByPathOrId = async function(pathOrId: string): Promise<PageDocument[]> {
+  let path;
+  if (!hasSlash(pathOrId)) {
+    const _id = pathOrId;
+    const page = await this.findOne({ _id });
+    if (page == null) throw Error('Page not found.');
+
+    path = page.path;
+  }
+  else {
+    path = pathOrId;
+  }
+
+  const ancestorPaths = collectAncestorPaths(path);
+
+  // Do not populate
+  const queryBuilder = new PageQueryBuilder(this.find());
+  const _ancestors: PageDocument[] = await queryBuilder
+    .addConditionToListByPathsArray(ancestorPaths)
+    .addConditionToSortAncestorPages()
+    .query
+    .lean()
+    .exec();
+
+  // no same path pages
+  const ancestorsMap = new Map<string, PageDocument>();
+  _ancestors.forEach(page => ancestorsMap.set(page.path, page));
+  const ancestors = Array.from(ancestorsMap.values());
+
+  return ancestors;
+};
+
 
 
 /*
 /*
  * Merge obsolete page model methods and define new methods which depend on crowi instance
  * Merge obsolete page model methods and define new methods which depend on crowi instance

+ 4 - 0
packages/app/src/server/routes/apiv3/index.js

@@ -1,5 +1,7 @@
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
+import pageListing from './page-listing';
+
 const logger = loggerFactory('growi:routes:apiv3'); // eslint-disable-line no-unused-vars
 const logger = loggerFactory('growi:routes:apiv3'); // eslint-disable-line no-unused-vars
 
 
 const express = require('express');
 const express = require('express');
@@ -41,6 +43,8 @@ module.exports = (crowi) => {
   router.use('/pages', require('./pages')(crowi));
   router.use('/pages', require('./pages')(crowi));
   router.use('/revisions', require('./revisions')(crowi));
   router.use('/revisions', require('./revisions')(crowi));
 
 
+  router.use('/page-listing', pageListing(crowi));
+
   router.use('/share-links', require('./share-links')(crowi));
   router.use('/share-links', require('./share-links')(crowi));
 
 
   router.use('/bookmarks', require('./bookmarks')(crowi));
   router.use('/bookmarks', require('./bookmarks')(crowi));

+ 6 - 0
packages/app/src/server/routes/apiv3/interfaces/apiv3-response.ts

@@ -0,0 +1,6 @@
+import { Response } from 'express';
+
+export interface ApiV3Response extends Response {
+  apiv3(obj?: any, status?: number): any
+  apiv3Err(_err: any, status?: number, info?: any): any
+}

+ 99 - 0
packages/app/src/server/routes/apiv3/page-listing.ts

@@ -0,0 +1,99 @@
+import express, { Request, Router } from 'express';
+import { pagePathUtils } from '@growi/core';
+import { query, oneOf } from 'express-validator';
+
+import { PageDocument, PageModel } from '../../models/page';
+import ErrorV3 from '../../models/vo/error-apiv3';
+import loggerFactory from '../../../utils/logger';
+import Crowi from '../../crowi';
+import { ApiV3Response } from './interfaces/apiv3-response';
+
+const { isTopPage } = pagePathUtils;
+
+const logger = loggerFactory('growi:routes:apiv3:page-tree');
+
+/*
+ * Types & Interfaces
+ */
+interface AuthorizedRequest extends Request {
+  user?: any
+}
+
+/*
+ * Validators
+ */
+const validator = {
+  pageIdAndPathRequired: [
+    query('id').isMongoId().withMessage('id is required'),
+    query('path').isString().withMessage('path is required'),
+  ],
+  pageIdOrPathRequired: oneOf([
+    query('id').isMongoId(),
+    query('path').isString(),
+  ], 'id or path is required'),
+};
+
+/*
+ * Routes
+ */
+export default (crowi: Crowi): Router => {
+  const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
+  // Do not use loginRequired with isGuestAllowed true since page tree may show private page titles
+  const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
+  const apiV3FormValidator = require('../../middlewares/apiv3-form-validator')(crowi);
+
+  const router = express.Router();
+
+
+  // eslint-disable-next-line max-len
+  router.get('/siblings', accessTokenParser, loginRequiredStrictly, ...validator.pageIdAndPathRequired, apiV3FormValidator, async(req: AuthorizedRequest, res: ApiV3Response): Promise<any> => {
+    const { id, path } = req.query;
+
+    const Page: PageModel = crowi.model('Page');
+
+    let siblings: PageDocument[];
+    let target: PageDocument;
+    try {
+      siblings = await Page.findSiblingsByPathAndViewer(path as string, req.user);
+
+      target = siblings.filter(page => page._id.toString() === id)?.[0];
+      if (target == null) {
+        throw Error('Target must exist.');
+      }
+    }
+    catch (err) {
+      logger.error('Error occurred while finding pages.', err);
+      return res.apiv3Err(new ErrorV3('Error occurred while finding pages.'));
+    }
+
+    if (isTopPage(path as string)) {
+      siblings = siblings.filter(page => !isTopPage(page.path));
+    }
+
+    return res.apiv3({ target, siblings });
+  });
+
+  // eslint-disable-next-line max-len
+  router.get('/ancestors', accessTokenParser, loginRequiredStrictly, validator.pageIdOrPathRequired, apiV3FormValidator, async(req: AuthorizedRequest, res: ApiV3Response): Promise<any> => {
+    const { id, path } = req.query;
+
+    const Page: PageModel = crowi.model('Page');
+
+    let ancestors: PageDocument[];
+    try {
+      ancestors = await Page.findAncestorsByPathOrId((path || id) as string);
+
+      if (ancestors.length === 0 && !isTopPage(path as string)) {
+        throw Error('Ancestors must have at least one page.');
+      }
+    }
+    catch (err) {
+      logger.error('Error occurred while finding pages.', err);
+      return res.apiv3Err(new ErrorV3('Error occurred while finding pages.'));
+    }
+
+    return res.apiv3({ ancestors });
+  });
+
+  return router;
+};

+ 121 - 3
yarn.lock

@@ -1093,6 +1093,11 @@
     minimatch "^3.0.4"
     minimatch "^3.0.4"
     strip-json-comments "^3.1.1"
     strip-json-comments "^3.1.1"
 
 
+"@gar/promisify@^1.0.1":
+  version "1.1.2"
+  resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.2.tgz#30aa825f11d438671d585bd44e7fd564535fc210"
+  integrity sha512-82cpyJyKRoQoRi+14ibCeGPu0CwypgtBAdBhq1WfvagpCZNKqwXbKwXllYSMG91DhmG4jt9gN8eP6lGOtozuaw==
+
 "@godaddy/terminus@^4.9.0":
 "@godaddy/terminus@^4.9.0":
   version "4.9.0"
   version "4.9.0"
   resolved "https://registry.yarnpkg.com/@godaddy/terminus/-/terminus-4.9.0.tgz#c7de0b45ede05116854d1461832dd05df169f689"
   resolved "https://registry.yarnpkg.com/@godaddy/terminus/-/terminus-4.9.0.tgz#c7de0b45ede05116854d1461832dd05df169f689"
@@ -2165,6 +2170,14 @@
   resolved "https://registry.yarnpkg.com/@npmcli/ci-detect/-/ci-detect-1.3.0.tgz#6c1d2c625fb6ef1b9dea85ad0a5afcbef85ef22a"
   resolved "https://registry.yarnpkg.com/@npmcli/ci-detect/-/ci-detect-1.3.0.tgz#6c1d2c625fb6ef1b9dea85ad0a5afcbef85ef22a"
   integrity sha512-oN3y7FAROHhrAt7Rr7PnTSwrHrZVRTS2ZbyxeQwSSYD0ifwM3YNgQqbaRmjcWoPyq77MjchusjJDspbzMmip1Q==
   integrity sha512-oN3y7FAROHhrAt7Rr7PnTSwrHrZVRTS2ZbyxeQwSSYD0ifwM3YNgQqbaRmjcWoPyq77MjchusjJDspbzMmip1Q==
 
 
+"@npmcli/fs@^1.0.0":
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-1.0.0.tgz#589612cfad3a6ea0feafcb901d29c63fd52db09f"
+  integrity sha512-8ltnOpRR/oJbOp8vaGUnipOi3bqkcW+sLHFlyXIr08OGHmVJLB1Hn7QtGXbYcpVtH1gAYZTlmDXtE4YV0+AMMQ==
+  dependencies:
+    "@gar/promisify" "^1.0.1"
+    semver "^7.3.5"
+
 "@npmcli/git@^2.0.1":
 "@npmcli/git@^2.0.1":
   version "2.0.6"
   version "2.0.6"
   resolved "https://registry.yarnpkg.com/@npmcli/git/-/git-2.0.6.tgz#47b97e96b2eede3f38379262fa3bdfa6eae57bf2"
   resolved "https://registry.yarnpkg.com/@npmcli/git/-/git-2.0.6.tgz#47b97e96b2eede3f38379262fa3bdfa6eae57bf2"
@@ -3729,7 +3742,7 @@ after@0.8.2:
   version "0.8.2"
   version "0.8.2"
   resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f"
   resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f"
 
 
-agent-base@6:
+agent-base@6, agent-base@^6.0.2:
   version "6.0.2"
   version "6.0.2"
   resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
   resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
   integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
   integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
@@ -5216,6 +5229,30 @@ cacache@^15.0.5:
     tar "^6.0.2"
     tar "^6.0.2"
     unique-filename "^1.1.1"
     unique-filename "^1.1.1"
 
 
+cacache@^15.2.0:
+  version "15.3.0"
+  resolved "https://registry.yarnpkg.com/cacache/-/cacache-15.3.0.tgz#dc85380fb2f556fe3dda4c719bfa0ec875a7f1eb"
+  integrity sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==
+  dependencies:
+    "@npmcli/fs" "^1.0.0"
+    "@npmcli/move-file" "^1.0.1"
+    chownr "^2.0.0"
+    fs-minipass "^2.0.0"
+    glob "^7.1.4"
+    infer-owner "^1.0.4"
+    lru-cache "^6.0.0"
+    minipass "^3.1.1"
+    minipass-collect "^1.0.2"
+    minipass-flush "^1.0.5"
+    minipass-pipeline "^1.2.2"
+    mkdirp "^1.0.3"
+    p-map "^4.0.0"
+    promise-inflight "^1.0.1"
+    rimraf "^3.0.2"
+    ssri "^8.0.1"
+    tar "^6.0.2"
+    unique-filename "^1.1.1"
+
 cache-base@^1.0.1:
 cache-base@^1.0.1:
   version "1.0.1"
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2"
   resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2"
@@ -9918,7 +9955,7 @@ got@^8.3.2:
     url-parse-lax "^3.0.0"
     url-parse-lax "^3.0.0"
     url-to-options "^1.0.1"
     url-to-options "^1.0.1"
 
 
-graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.3, graceful-fs@^4.2.4:
+graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.3, graceful-fs@^4.2.4, graceful-fs@^4.2.6:
   version "4.2.8"
   version "4.2.8"
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a"
   resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.8.tgz#e412b8d33f5e006593cbd3cee6df9f2cebbe802a"
   integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==
   integrity sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==
@@ -10746,6 +10783,11 @@ inquirer@^7.0.0, inquirer@^7.3.3:
     strip-ansi "^6.0.0"
     strip-ansi "^6.0.0"
     through "^2.3.6"
     through "^2.3.6"
 
 
+install-artifact-from-github@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/install-artifact-from-github/-/install-artifact-from-github-1.2.0.tgz#adcbd123c16a4337ec44ea76d0ebf253cc16b074"
+  integrity sha512-3OxCPcY55XlVM3kkfIpeCgmoSKnMsz2A3Dbhsq0RXpIknKQmrX1YiznCeW9cD2ItFmDxziA3w6Eg8d80AoL3oA==
+
 internal-slot@^1.0.3:
 internal-slot@^1.0.3:
   version "1.0.3"
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c"
   resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c"
@@ -12967,6 +13009,28 @@ make-fetch-happen@^8.0.9:
     socks-proxy-agent "^5.0.0"
     socks-proxy-agent "^5.0.0"
     ssri "^8.0.0"
     ssri "^8.0.0"
 
 
+make-fetch-happen@^9.1.0:
+  version "9.1.0"
+  resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz#53085a09e7971433e6765f7971bf63f4e05cb968"
+  integrity sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==
+  dependencies:
+    agentkeepalive "^4.1.3"
+    cacache "^15.2.0"
+    http-cache-semantics "^4.1.0"
+    http-proxy-agent "^4.0.1"
+    https-proxy-agent "^5.0.0"
+    is-lambda "^1.0.1"
+    lru-cache "^6.0.0"
+    minipass "^3.1.3"
+    minipass-collect "^1.0.2"
+    minipass-fetch "^1.3.2"
+    minipass-flush "^1.0.5"
+    minipass-pipeline "^1.2.4"
+    negotiator "^0.6.2"
+    promise-retry "^2.0.1"
+    socks-proxy-agent "^6.0.0"
+    ssri "^8.0.0"
+
 makeerror@1.0.x:
 makeerror@1.0.x:
   version "1.0.11"
   version "1.0.11"
   resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c"
   resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.11.tgz#e01a5c9109f2af79660e4e8b9587790184f5a96c"
@@ -14234,7 +14298,7 @@ negotiator@0.6.1:
   version "0.6.1"
   version "0.6.1"
   resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
   resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9"
 
 
-negotiator@0.6.2:
+negotiator@0.6.2, negotiator@^0.6.2:
   version "0.6.2"
   version "0.6.2"
   resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
   resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.2.tgz#feacf7ccf525a77ae9634436a64883ffeca346fb"
   integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
   integrity sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
@@ -14401,6 +14465,22 @@ node-gyp@^7.1.0:
     tar "^6.0.2"
     tar "^6.0.2"
     which "^2.0.2"
     which "^2.0.2"
 
 
+node-gyp@^8.0.0:
+  version "8.4.0"
+  resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-8.4.0.tgz#6e1112b10617f0f8559c64b3f737e8109e5a8338"
+  integrity sha512-Bi/oCm5bH6F+FmzfUxJpPaxMEyIhszULGR3TprmTeku8/dMFcdTcypk120NeZqEt54r1BrgEKtm2jJiuIKE28Q==
+  dependencies:
+    env-paths "^2.2.0"
+    glob "^7.1.4"
+    graceful-fs "^4.2.6"
+    make-fetch-happen "^9.1.0"
+    nopt "^5.0.0"
+    npmlog "^4.1.2"
+    rimraf "^3.0.2"
+    semver "^7.3.5"
+    tar "^6.1.2"
+    which "^2.0.2"
+
 node-int64@^0.4.0:
 node-int64@^0.4.0:
   version "0.4.0"
   version "0.4.0"
   resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
   resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
@@ -17027,6 +17107,15 @@ rc@>=1.2.8, rc@^1.0.1, rc@^1.1.6, rc@^1.2.7:
     minimist "^1.2.0"
     minimist "^1.2.0"
     strip-json-comments "~2.0.1"
     strip-json-comments "~2.0.1"
 
 
+re2@^1.16.0:
+  version "1.16.0"
+  resolved "https://registry.yarnpkg.com/re2/-/re2-1.16.0.tgz#f311eb4865b1296123800ea8e013cec8dab25590"
+  integrity sha512-eizTZL2ZO0ZseLqfD4t3Qd0M3b3Nr0MBWpX81EbPMIud/1d/CSfUIx2GQK8fWiAeHoSekO5EOeFib2udTZLwYw==
+  dependencies:
+    install-artifact-from-github "^1.2.0"
+    nan "^2.14.2"
+    node-gyp "^8.0.0"
+
 react-addons-text-content@^0.0.4:
 react-addons-text-content@^0.0.4:
   version "0.0.4"
   version "0.0.4"
   resolved "https://registry.yarnpkg.com/react-addons-text-content/-/react-addons-text-content-0.0.4.tgz#d2e259fdc951d1d8906c08902002108dce8792e5"
   resolved "https://registry.yarnpkg.com/react-addons-text-content/-/react-addons-text-content-0.0.4.tgz#d2e259fdc951d1d8906c08902002108dce8792e5"
@@ -19100,6 +19189,15 @@ socks-proxy-agent@^5.0.0:
     debug "4"
     debug "4"
     socks "^2.3.3"
     socks "^2.3.3"
 
 
+socks-proxy-agent@^6.0.0:
+  version "6.1.0"
+  resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-6.1.0.tgz#869cf2d7bd10fea96c7ad3111e81726855e285c3"
+  integrity sha512-57e7lwCN4Tzt3mXz25VxOErJKXlPfXmkMLnk310v/jwW20jWRVcgsOit+xNkN3eIEdB47GwnfAEBLacZ/wVIKg==
+  dependencies:
+    agent-base "^6.0.2"
+    debug "^4.3.1"
+    socks "^2.6.1"
+
 socks@^2.3.3:
 socks@^2.3.3:
   version "2.6.0"
   version "2.6.0"
   resolved "https://registry.yarnpkg.com/socks/-/socks-2.6.0.tgz#6b984928461d39871b3666754b9000ecf39dfac2"
   resolved "https://registry.yarnpkg.com/socks/-/socks-2.6.0.tgz#6b984928461d39871b3666754b9000ecf39dfac2"
@@ -19108,6 +19206,14 @@ socks@^2.3.3:
     ip "^1.1.5"
     ip "^1.1.5"
     smart-buffer "^4.1.0"
     smart-buffer "^4.1.0"
 
 
+socks@^2.6.1:
+  version "2.6.1"
+  resolved "https://registry.yarnpkg.com/socks/-/socks-2.6.1.tgz#989e6534a07cf337deb1b1c94aaa44296520d30e"
+  integrity sha512-kLQ9N5ucj8uIcxrDwjm0Jsqk06xdpBjGNQtpXy4Q8/QY2k+fY7nZH8CARy+hkbG+SGAovmzzuauCpBlb8FrnBA==
+  dependencies:
+    ip "^1.1.5"
+    smart-buffer "^4.1.0"
+
 sort-keys@^1.0.0:
 sort-keys@^1.0.0:
   version "1.1.2"
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad"
   resolved "https://registry.yarnpkg.com/sort-keys/-/sort-keys-1.1.2.tgz#441b6d4d346798f1b4e49e8920adfba0e543f9ad"
@@ -20224,6 +20330,18 @@ tar@^6.1.0:
     mkdirp "^1.0.3"
     mkdirp "^1.0.3"
     yallist "^4.0.0"
     yallist "^4.0.0"
 
 
+tar@^6.1.2:
+  version "6.1.11"
+  resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.11.tgz#6760a38f003afa1b2ffd0ffe9e9abbd0eab3d621"
+  integrity sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==
+  dependencies:
+    chownr "^2.0.0"
+    fs-minipass "^2.0.0"
+    minipass "^3.0.0"
+    minizlib "^2.1.1"
+    mkdirp "^1.0.3"
+    yallist "^4.0.0"
+
 tdigest@^0.1.1:
 tdigest@^0.1.1:
   version "0.1.1"
   version "0.1.1"
   resolved "https://registry.yarnpkg.com/tdigest/-/tdigest-0.1.1.tgz#2e3cb2c39ea449e55d1e6cd91117accca4588021"
   resolved "https://registry.yarnpkg.com/tdigest/-/tdigest-0.1.1.tgz#2e3cb2c39ea449e55d1e6cd91117accca4588021"