diff --git a/.github/workflows/ci_e2e_puppeteer.yml b/.github/workflows/ci_e2e_puppeteer.yml index e38f97fcfe..66666a5941 100644 --- a/.github/workflows/ci_e2e_puppeteer.yml +++ b/.github/workflows/ci_e2e_puppeteer.yml @@ -22,7 +22,10 @@ jobs: ports: - 27017/tcp steps: - - uses: actions/checkout@v2 + - name: Checkout reposistory + uses: actions/checkout@v2 + - name: Checkout submodules + run: git submodule update --init --recursive - name: Use Node.js 14.6.x uses: actions/setup-node@v1 with: @@ -51,11 +54,6 @@ jobs: - name: build production if: steps.cache-build.outputs.cache-hit != 'true' run: yarn production-build - - name: checkout fixtures - uses: actions/checkout@v2 - with: - repository: huridocs/uwazi-fixtures - path: uwazi-fixtures - run: yarn e2e-puppeteer-fixtures env: DBHOST: localhost:${{ job.services.mongodb.ports[27017] }} diff --git a/app/api/migrations/migrate.ts b/app/api/migrations/migrate.ts index 376986c1c8..3284ff614b 100644 --- a/app/api/migrations/migrate.ts +++ b/app/api/migrations/migrate.ts @@ -1,3 +1,4 @@ +import { ConnectionOptions } from 'mongoose'; import { DB } from 'api/odm'; import { tenants } from 'api/tenants/tenantContext'; import { config } from 'api/config'; @@ -8,8 +9,17 @@ process.on('unhandledRejection', error => { throw error; }); +let auth: ConnectionOptions; + +if (process.env.DBUSER) { + auth = { + user: process.env.DBUSER, + pass: process.env.DBPASS, + }; +} + const run = async () => { - await DB.connect(); + await DB.connect(config.DBHOST, auth); const { db } = await DB.connectionForDB(config.defaultTenant.dbName); await tenants.run(async () => { diff --git a/app/api/odm/DB.ts b/app/api/odm/DB.ts index ab568db194..f35d71450c 100644 --- a/app/api/odm/DB.ts +++ b/app/api/odm/DB.ts @@ -1,18 +1,13 @@ -import mongoose, { Connection } from 'mongoose'; +import mongoose, { Connection, ConnectionOptions } from 'mongoose'; import { config } from 'api/config'; -type dbAuth = { - user: string; - pass: string; -}; - let connection: Connection; // setting this on createConnection directly is not working, maybe mongoose bug? mongoose.set('useCreateIndex', true); const DB = { - async connect(uri: string = config.DBHOST, auth?: dbAuth) { + async connect(uri: string = config.DBHOST, auth?: ConnectionOptions) { connection = await mongoose.createConnection(uri, { ...auth, useUnifiedTopology: true, diff --git a/app/api/socketio/specs/socketClusterMode.spec.ts b/app/api/socketio/specs/socketClusterMode.spec.ts index 87f096cb20..41756f95b7 100644 --- a/app/api/socketio/specs/socketClusterMode.spec.ts +++ b/app/api/socketio/specs/socketClusterMode.spec.ts @@ -6,6 +6,7 @@ import { multitenantMiddleware } from 'api/utils/multitenantMiddleware'; import { tenants, Tenant } from 'api/tenants/tenantContext'; import { setupSockets } from '../setupSockets'; +import { appContextMiddleware } from 'api/utils/appContextMiddleware'; const closeServer = async (httpServer: Server) => new Promise(resolve => { @@ -55,6 +56,7 @@ const app: Application = express(); describe('socket middlewares setup', () => { beforeAll(async () => { server = await createServer(app, port); + app.use(appContextMiddleware); app.use(multitenantMiddleware); setupSockets(server, app); diff --git a/app/api/sync/specs/uploadRoute.spec.ts b/app/api/sync/specs/uploadRoute.spec.ts index 2fc32872a9..52420e5e30 100644 --- a/app/api/sync/specs/uploadRoute.spec.ts +++ b/app/api/sync/specs/uploadRoute.spec.ts @@ -10,6 +10,7 @@ import { testingTenants } from 'api/utils/testingTenants'; import { multitenantMiddleware } from 'api/utils/multitenantMiddleware'; import syncRoutes from '../routes'; +import { appContextMiddleware } from 'api/utils/appContextMiddleware'; jest.mock( '../../auth/authMiddleware.ts', @@ -33,6 +34,7 @@ describe('sync', () => { await deleteFile(uploadsPath('testUpload.txt')); const app = express(); + app.use(appContextMiddleware); app.use(multitenantMiddleware); syncRoutes(app); diff --git a/app/api/tenants/tenantContext.ts b/app/api/tenants/tenantContext.ts index 3e5279d240..ccc8dacced 100644 --- a/app/api/tenants/tenantContext.ts +++ b/app/api/tenants/tenantContext.ts @@ -1,6 +1,6 @@ -import { AsyncLocalStorage } from 'async_hooks'; import { config } from 'api/config'; import handleError from 'api/utils/handleError.js'; +import { appContext } from 'api/utils/AppContext'; import { TenantsModel } from './tenantsModel'; export type Tenant = { @@ -14,8 +14,6 @@ export type Tenant = { }; class Tenants { - storage = new AsyncLocalStorage(); - tenants: { [k: string]: Tenant }; constructor() { @@ -40,21 +38,24 @@ class Tenants { }); } + /** + * This is a proxy to the context run method using only the tenant information. + * It is here for backwards compatibility after refactoring. + * @param cb The callback to run in the context + * @param tenantName Tenant name + */ + // eslint-disable-next-line class-methods-use-this async run( cb: () => Promise, tenantName: string = config.defaultTenant.name ): Promise { - return new Promise((resolve, reject) => { - this.storage.run(tenantName, () => { - cb() - .then(resolve) - .catch(reject); - }); + return appContext.run(cb, { + tenant: tenantName, }); } current() { - const tenantName = this.storage.getStore(); + const tenantName = appContext.get('tenant'); if (!tenantName) { throw new Error('There is no tenant on the current async context'); diff --git a/app/api/utils/AppContext.ts b/app/api/utils/AppContext.ts new file mode 100644 index 0000000000..7d2d096a1f --- /dev/null +++ b/app/api/utils/AppContext.ts @@ -0,0 +1,37 @@ +import { AsyncLocalStorage } from 'async_hooks'; + +interface ContextData { + [k: string]: unknown; +} + +class AppContext { + private storage = new AsyncLocalStorage(); + + private getContextObject() { + const data = this.storage.getStore(); + if (!data) throw new Error('Accessing nonexistent async context'); + return data; + } + + async run(cb: () => Promise, data: ContextData = {}): Promise { + return new Promise((resolve, reject) => { + this.storage.run(data, () => { + cb() + .then(resolve) + .catch(reject); + }); + }); + } + + get(key: string) { + return this.getContextObject()[key]; + } + + set(key: string, value: unknown) { + this.getContextObject()[key] = value; + } +} + +const appContext = new AppContext(); + +export { appContext }; diff --git a/app/api/utils/appContextMiddleware.ts b/app/api/utils/appContextMiddleware.ts new file mode 100644 index 0000000000..a918e76a97 --- /dev/null +++ b/app/api/utils/appContextMiddleware.ts @@ -0,0 +1,12 @@ +import { Request, Response, NextFunction } from 'express'; +import { appContext } from 'api/utils/AppContext'; + +const appContextMiddleware = (_req: Request, _res: Response, next: NextFunction) => { + appContext + .run(async () => { + next(); + }) + .catch(next); +}; + +export { appContextMiddleware }; diff --git a/app/api/utils/multitenantMiddleware.ts b/app/api/utils/multitenantMiddleware.ts index 2857cc16f0..9198b67104 100644 --- a/app/api/utils/multitenantMiddleware.ts +++ b/app/api/utils/multitenantMiddleware.ts @@ -1,12 +1,10 @@ import { Request, Response, NextFunction } from 'express'; -import { tenants } from 'api/tenants/tenantContext'; +import { appContext } from 'api/utils/AppContext'; +import { config } from 'api/config'; const multitenantMiddleware = (req: Request, _res: Response, next: NextFunction) => { - tenants - .run(async () => { - next(); - }, req.get('tenant')) - .catch(next); + appContext.set('tenant', req.get('tenant') || config.defaultTenant.name); + next(); }; export { multitenantMiddleware }; diff --git a/app/api/utils/specs/appContext.spec.ts b/app/api/utils/specs/appContext.spec.ts new file mode 100644 index 0000000000..9c88db75ce --- /dev/null +++ b/app/api/utils/specs/appContext.spec.ts @@ -0,0 +1,66 @@ +import { appContext } from '../AppContext'; + +describe('appContext', () => { + describe('when running the callback inside a context', () => { + it('it should access the given values', async () => { + await appContext.run( + async () => { + expect(appContext.get('key1')).toBe('value1'); + expect(appContext.get('key2')).toBe('value2'); + }, + { + key1: 'value1', + key2: 'value2', + } + ); + }); + + it('it should return undefined if accessing an unexising key', async () => { + await appContext.run( + async () => { + expect(appContext.get('non-existing')).toBe(undefined); + }, + { + key: 'value', + } + ); + }); + + it('it should set a new key', async () => { + await appContext.run(async () => { + expect(appContext.get('someKey')).toBe(undefined); + appContext.set('someKey', 'someValue'); + expect(appContext.get('someKey')).toBe('someValue'); + }); + }); + + it('it should overwrite existing keys', async () => { + await appContext.run( + async () => { + expect(appContext.get('someKey')).toBe('previous'); + appContext.set('someKey', 'someValue'); + expect(appContext.get('someKey')).toBe('someValue'); + }, + { + someKey: 'previous', + } + ); + }); + }); + + describe('when outside a context', () => { + const error = new Error('Accessing nonexistent async context'); + + it('should throw on get', () => { + expect(() => { + appContext.get('somKey'); + }).toThrow(error); + }); + + it('should throw on set', () => { + expect(() => { + appContext.set('somKey', 'someValue'); + }).toThrow(error); + }); + }); +}); diff --git a/app/api/utils/specs/appContextMiddleware.spec.ts b/app/api/utils/specs/appContextMiddleware.spec.ts new file mode 100644 index 0000000000..683c3bb2a4 --- /dev/null +++ b/app/api/utils/specs/appContextMiddleware.spec.ts @@ -0,0 +1,31 @@ +import request from 'supertest'; +import express, { Application, Request, Response, NextFunction } from 'express'; +import { appContextMiddleware } from '../appContextMiddleware'; +import { appContext } from '../AppContext'; + +const testingRoutes = (app: Application) => { + app.get('/api/testGET', (_req, res, next) => { + res.json(appContext.get('someKey')); + next(); + }); +}; + +const helperMiddleware = (req: Request, _res: Response, next: NextFunction) => { + appContext.set('someKey', req.get('someHeader')); + next(); +}; + +describe('appcontext middleware', () => { + it('should execute next middlewares inside an async context', async () => { + const app: Application = express(); + app.use(appContextMiddleware); + app.use(helperMiddleware); + testingRoutes(app); + + const response = await request(app) + .get('/api/testGET') + .set('someHeader', 'test'); + + expect(response.text).toBe(JSON.stringify('test')); + }); +}); diff --git a/app/api/utils/specs/multitenantMiddleware.spec.ts b/app/api/utils/specs/multitenantMiddleware.spec.ts index 65a158d631..919488d116 100644 --- a/app/api/utils/specs/multitenantMiddleware.spec.ts +++ b/app/api/utils/specs/multitenantMiddleware.spec.ts @@ -2,6 +2,7 @@ import request from 'supertest'; import express, { Application } from 'express'; import { tenants } from 'api/tenants/tenantContext'; import { multitenantMiddleware } from '../multitenantMiddleware'; +import { appContextMiddleware } from '../appContextMiddleware'; const testingRoutes = (app: Application) => { app.get('/api/testGET', (_req, res, next) => { @@ -16,6 +17,7 @@ describe('multitenant middleware', () => { tenants.add({ name: 'test' }); const app: Application = express(); + app.use(appContextMiddleware); app.use(multitenantMiddleware); testingRoutes(app); diff --git a/app/server.js b/app/server.js index 79e6043b62..bd7cf6c511 100644 --- a/app/server.js +++ b/app/server.js @@ -10,6 +10,7 @@ import path from 'path'; import { TaskProvider } from 'shared/tasks/tasks'; +import { appContextMiddleware } from 'api/utils/appContextMiddleware'; import uwaziMessage from '../message'; import apiRoutes from './api/api'; import privateInstanceMiddleware from './api/auth/privateInstanceMiddleware'; @@ -57,6 +58,8 @@ app.use(express.static(path.resolve(__dirname, '../dist'), { maxage })); app.use('/public', express.static(config.publicAssets)); app.use(/\/((?!remotepublic).)*/, bodyParser.json({ limit: '1mb' })); +app.use(appContextMiddleware); + ////// // this middleware should go just before any other that accesses to db app.use(multitenantMiddleware); diff --git a/database/reindex_elastic.js b/database/reindex_elastic.js index e5c9035faf..d4bc9d1d74 100644 --- a/database/reindex_elastic.js +++ b/database/reindex_elastic.js @@ -116,7 +116,17 @@ process.on('unhandledRejection', error => { throw error; }); -DB.connect().then(async () => { +let dbAuth = {}; + +if (process.env.DBUSER) { + dbAuth = { + auth: { authSource: 'admin' }, + user: process.env.DBUSER, + pass: process.env.DBPASS, + }; +} + +DB.connect(config.DBHOST, dbAuth).then(async () => { const start = Date.now(); await tenants.run(async () => { diff --git a/e2e/helpers/disableTransitions.ts b/e2e/helpers/disableTransitions.ts new file mode 100644 index 0000000000..cd34119971 --- /dev/null +++ b/e2e/helpers/disableTransitions.ts @@ -0,0 +1,17 @@ +/*global page*/ + +export default async () => { + await page.addStyleTag({ + content: ` + *, + *::after, + *::before { + transition-delay: 0s !important; + transition-duration: 0s !important; + animation-delay: -0.0001s !important; + animation-duration: 0s !important; + animation-play-state: paused !important; + caret-color: transparent !important; + }`, + }); +}; diff --git a/e2e/suites/copy-from.test.ts b/e2e/suites/copy-from.test.ts index 34e5c729e1..c7dca3c4e9 100644 --- a/e2e/suites/copy-from.test.ts +++ b/e2e/suites/copy-from.test.ts @@ -3,6 +3,7 @@ import { adminLogin, logout } from '../helpers/login'; import proxyMock from '../helpers/proxyMock'; import insertFixtures from '../helpers/insertFixtures'; +import disableTransitions from '../helpers/disableTransitions'; import { host } from '../config'; describe('Copy from', () => { @@ -10,6 +11,7 @@ describe('Copy from', () => { await insertFixtures(); await proxyMock(); await adminLogin(); + await disableTransitions(); await page.goto(`${host}/library`); }); diff --git a/e2e/suites/default-library-view.test.ts b/e2e/suites/default-library-view.test.ts index 90445f9ca7..0508818cb0 100644 --- a/e2e/suites/default-library-view.test.ts +++ b/e2e/suites/default-library-view.test.ts @@ -4,12 +4,14 @@ import { host } from '../config'; import { adminLogin, logout } from '../helpers/login'; import proxyMock from '../helpers/proxyMock'; import insertFixtures from '../helpers/insertFixtures'; +import disableTransitions from '../helpers/disableTransitions'; describe('Entities', () => { beforeAll(async () => { await insertFixtures(); await proxyMock(); await adminLogin(); + await disableTransitions(); }); it('Should set the default library view to Table', async () => { diff --git a/e2e/suites/entity.test.ts b/e2e/suites/entity.test.ts index 02cb1d5dfe..3c70445816 100644 --- a/e2e/suites/entity.test.ts +++ b/e2e/suites/entity.test.ts @@ -3,12 +3,14 @@ import { adminLogin, logout } from '../helpers/login'; import proxyMock from '../helpers/proxyMock'; import insertFixtures from '../helpers/insertFixtures'; +import disableTransitions from '../helpers/disableTransitions'; describe('Entities', () => { beforeAll(async () => { await insertFixtures(); await proxyMock(); await adminLogin(); + await disableTransitions(); }); it('Should create new entity', async () => { diff --git a/e2e/suites/graphs.test.ts b/e2e/suites/graphs.test.ts index ce573aab72..1fabba6448 100644 --- a/e2e/suites/graphs.test.ts +++ b/e2e/suites/graphs.test.ts @@ -6,6 +6,7 @@ import { adminLogin, logout } from '../helpers/login'; import proxyMock from '../helpers/proxyMock'; import insertFixtures from '../helpers/insertFixtures'; import { displayGraph } from '../helpers/graphs'; +import disableTransitions from '../helpers/disableTransitions'; const localSelectors = { pageContentsInput: @@ -24,6 +25,7 @@ describe('Graphs in Page', () => { await insertFixtures(); await proxyMock(); await adminLogin(); + await disableTransitions(); }); it('should create a basic page', async () => { diff --git a/e2e/suites/login.test.ts b/e2e/suites/login.test.ts index dc7f112cc7..c5dcdbdd32 100644 --- a/e2e/suites/login.test.ts +++ b/e2e/suites/login.test.ts @@ -4,11 +4,13 @@ import { host } from '../config'; import { adminLogin, logout } from '../helpers/login'; import proxyMock from '../helpers/proxyMock'; import insertFixtures from '../helpers/insertFixtures'; +import disableTransitions from '../helpers/disableTransitions'; describe('Login', () => { beforeAll(async () => { await insertFixtures(); await proxyMock(); + await disableTransitions(); }); it('Should login as admin', async () => { diff --git a/e2e/suites/tableView.test.ts b/e2e/suites/tableView.test.ts index bb6ac2db9b..3486abb0d1 100644 --- a/e2e/suites/tableView.test.ts +++ b/e2e/suites/tableView.test.ts @@ -4,12 +4,14 @@ import { host } from '../config'; import proxyMock from '../helpers/proxyMock'; import insertFixtures from '../helpers/insertFixtures'; import { adminLogin, logout } from '../helpers/login'; +import disableTransitions from '../helpers/disableTransitions'; describe('Table view', () => { beforeAll(async () => { await insertFixtures(); await proxyMock(); await adminLogin(); + await disableTransitions(); }); it('Should go to the table view', async () => { diff --git a/nightmare/helpers/connectionsDSL.js b/nightmare/helpers/connectionsDSL.js index 3a10e4b4c4..05b3956bc1 100644 --- a/nightmare/helpers/connectionsDSL.js +++ b/nightmare/helpers/connectionsDSL.js @@ -157,9 +157,11 @@ Nightmare.action('connections', { .catch(done); }, sidePanelSearchAndSelect(term, done) { + const searchTerm = term.searchTerm || term; + const textOnDom = term.textOnDom || term; this.connections - .sidepanelSearch(term) - .connections.sidepanelSelect(term) + .sidepanelSearch(searchTerm) + .connections.sidepanelSelect(textOnDom) .then(() => { done(); }) @@ -167,43 +169,36 @@ Nightmare.action('connections', { }, sidepanelSearch(term, done) { this.clearInput(selectors.connections.sidePanelSearchInput) - .write(selectors.connections.sidePanelSearchInput, term) + .write(selectors.connections.sidePanelSearchInput, `"${term}"`) .then(() => { done(); }) .catch(done); }, + sidepanelSelect(matchingTitle, done) { - this.wait(selectors.connections.sidePanelFirstDocument) - .wait( - (termToMatch, selector) => { - const element = document.querySelectorAll(selector)[0]; - if (element) { - return element.innerText.toLowerCase().match(termToMatch.toLowerCase()); - } - return false; - }, - matchingTitle, - selectors.connections.sidePanelDocuments - ) - .evaluate( - (toMatch, selector) => { - const helpers = document.__helpers; - const elements = helpers.querySelectorAll(selector); - let found; - elements.forEach(element => { - if (found) { - return; - } - if (element.innerText.toLowerCase() === toMatch.toLowerCase()) { - found = element; - } - }); - found.click(); - }, - matchingTitle, - selectors.connections.sidePanelDocuments - ) + this.wait(toMatch => { + const elements = document.querySelectorAll( + '#app > div.content > div > div > aside.side-panel.create-reference.is-active > div.sidepanel-body > div > div > div.item' + ); + + let found; + elements.forEach(element => { + if (found) { + return; + } + if (element.innerText.replace(/(\r\n|\n|\r)/gm, ' ').match(new RegExp(toMatch, 'i'))) { + found = element; + } + }); + + if (found) { + found.click(); + return true; + } + + return false; + }, matchingTitle) .then(() => { done(); }) diff --git a/nightmare/suite1/connections.spec.js b/nightmare/suite1/connections.spec.js index 4699f8506e..8222db6b24 100644 --- a/nightmare/suite1/connections.spec.js +++ b/nightmare/suite1/connections.spec.js @@ -55,10 +55,10 @@ describe('Connections', () => { it('should add the perpetrators', done => { nightmare.connections - .sidePanelSearchAndSelect('joker') + .sidePanelSearchAndSelect({ searchTerm: 'joker', textOnDom: 'Super Villian' }) .connections.sidePanelSearchAndSelect('scarecrow') .connections.sidePanelSearchAndSelect("Ra's al Ghul") - .connections.sidePanelSearchAndSelect('robin') + .connections.sidePanelSearchAndSelect({ searchTerm: 'robin', textOnDom: 'robin comic character' }) .connections.sidePanelSearchAndSelect('Talia al Ghul') .connections.sidePanelSearchAndSelect('Cluemaster Wikipedia') .then(() => { @@ -78,7 +78,7 @@ describe('Connections', () => { it('should add the heros', done => { nightmare.connections - .sidePanelSearchAndSelect('batman') + .sidePanelSearchAndSelect({ searchTerm: 'batman', textOnDom: 'batman comic character' }) .connections.sidePanelSearchAndSelect('alfred pennyworth') .then(() => { done(); diff --git a/package.json b/package.json index 5dc6bbb9db..4001bb7296 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uwazi", - "version": "1.20.0", + "version": "1.21.0", "description": "Uwazi is a free, open-source solution for organising, analysing and publishing your documents.", "keywords": [ "react" @@ -56,6 +56,10 @@ "app/jest.server.config.js" ] }, + "resolutions": { + "lodash": "^4.17.20", + "bl": "^4.0.3" + }, "dependencies": { "@babel/register": "7.11.5", "@elastic/elasticsearch": "^7.9.0", diff --git a/uwazi-fixtures b/uwazi-fixtures index a6e29a4f79..b7fefead98 160000 --- a/uwazi-fixtures +++ b/uwazi-fixtures @@ -1 +1 @@ -Subproject commit a6e29a4f79dac849c546262f8c29ed96af187f18 +Subproject commit b7fefead9820b1f7f1e5ade2a23d91d50d8fba72 diff --git a/yarn.lock b/yarn.lock index 4c7d838b6f..872793a07a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3086,23 +3086,7 @@ binary-extensions@^2.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.1.0.tgz#30fa40c9e7fe07dbc895678cd287024dea241dd9" integrity sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ== -bl@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/bl/-/bl-2.2.0.tgz#e1a574cdf528e4053019bb800b041c0ac88da493" - integrity sha512-wbgvOpqopSr7uq6fJrLH8EsvYMJf9gzfo2jCsL2eTy75qXPukA4pCgHamOQkZtY5vmfVtjB+P3LNlMHW5CEZXA== - dependencies: - readable-stream "^2.3.5" - safe-buffer "^5.1.1" - -bl@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/bl/-/bl-2.2.1.tgz#8c11a7b730655c5d56898cdc871224f40fd901d5" - integrity sha512-6Pesp1w0DEX1N550i/uGV/TqucVL4AM/pgThFSN/Qq9si1/DF9aIHs1BxD8V/QU0HoeHO6cQRTAuYnLPKq1e4g== - dependencies: - readable-stream "^2.3.5" - safe-buffer "^5.1.1" - -bl@^4.0.3: +bl@^2.2.0, bl@^2.2.1, bl@^4.0.3: version "4.0.3" resolved "https://registry.yarnpkg.com/bl/-/bl-4.0.3.tgz#12d6287adc29080e22a705e5764b2a9522cdc489" integrity sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg== @@ -9037,32 +9021,11 @@ lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" -lodash@3.0.x: - version "3.0.1" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.0.1.tgz#14d49028a38bc740241d11e2ecd57ec06d73c19a" - -lodash@^4.0.0, lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.6.1, lodash@~4.17.4: - version "4.17.5" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511" - -lodash@^4.0.1, lodash@^4.17.11, lodash@^4.17.3: - version "4.17.11" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" - -lodash@^4.17.10: - version "4.17.10" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" - -lodash@^4.17.14, lodash@^4.17.19, lodash@^4.17.20: +lodash@3.0.x, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.2.1, lodash@^4.6.1, lodash@~4.17.4: version "4.17.20" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== -lodash@^4.17.15: - version "4.17.15" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" - integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== - log-symbols@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a"