diff --git a/.env.sample b/.env.sample index 01e62ea..7d0590a 100644 --- a/.env.sample +++ b/.env.sample @@ -34,3 +34,7 @@ EXPRESS_LOGS_FILE_PATH='/home/.express/reveal-express-server.log # https://github.com/helmetjs/helmet#reference EXPRESS_CONTENT_SECURITY_POLICY_CONFIG=`{"default-src":["'self'"]}` + +EXPRESS_REDIS_STAND_ALONE_URL=redis://username:authpassword@127.0.0.1:6379/4 + +EXPRESS_REDIS_SENTINEL_CONFIG='{"name":"master","sentinelUsername":"u_name","sentinelPassword":"pass","db":4,"sentinels":[{"host":"127.0.0.1","port":6379},{"host":"127.0.0.1","port":6379}]}' diff --git a/.eslintignore b/.eslintignore index 21c4329..87cd9d0 100644 --- a/.eslintignore +++ b/.eslintignore @@ -3,3 +3,4 @@ dist coverage .eslintrc.js jest.config.js +jest.setup.js diff --git a/jest.config.js b/jest.config.js index 11d086f..b530cff 100644 --- a/jest.config.js +++ b/jest.config.js @@ -9,4 +9,5 @@ module.exports = { testEnvironment: 'node', collectCoverageFrom: ['src/**/*.{ts,tsx}', '!src/**/tests/', '!src/index.ts'], coverageReporters: ['lcov', 'html'], + setupFilesAfterEnv: ['/jest.setup.js'], }; diff --git a/jest.setup.js b/jest.setup.js new file mode 100644 index 0000000..17aef4e --- /dev/null +++ b/jest.setup.js @@ -0,0 +1,4 @@ +global.console = { + ...console, + log: jest.fn(), +}; diff --git a/package.json b/package.json index 6d0f0e9..74a7fbb 100644 --- a/package.json +++ b/package.json @@ -27,11 +27,13 @@ "@onaio/gatekeeper": "^0.1.2", "client-oauth2": "^4.3.3", "compression": "^1.7.4", + "connect-redis": "^6.1.3", "cookie-parser": "^1.4.6", "dotenv": "^16.0.0", "express": "^4.17.3", "express-session": "^1.17.2", "helmet": "^5.0.2", + "ioredis": "^5.0.6", "morgan": "^1.10.0", "node-fetch": "2.6.7", "react": "^17.0.2", @@ -39,6 +41,7 @@ "react-redux": "^7.2.6", "react-router": "^5.2.1", "react-router-dom": "^5.2.1", + "redis": "^4.1.0", "redux": "^4.1.2", "request": "^2.88.0", "seamless-immutable": "^7.1.4", @@ -48,10 +51,12 @@ }, "devDependencies": { "@types/compression": "^1.7.2", + "@types/connect-redis": "^0.0.18", "@types/cookie-parser": "^1.4.2", "@types/express": "^4.17.13", "@types/express-session": "^1.17.4", "@types/helmet": "^4.0.0", + "@types/ioredis-mock": "^5.6.0", "@types/jest": "^27.4.1", "@types/morgan": "^1.9.3", "@types/node": "^17.0.21", @@ -75,6 +80,7 @@ "eslint-plugin-prettier": "^4.0.0", "eslint-plugin-sonarjs": "^0.12.0", "husky": "^7.0.4", + "ioredis-mock": "^8.2.2", "jest": "^27.5.1", "lint-staged": "^12.3.4", "mockdate": "^3.0.5", diff --git a/src/app/index.ts b/src/app/index.ts index ad0d2e2..9593120 100644 --- a/src/app/index.ts +++ b/src/app/index.ts @@ -4,6 +4,8 @@ import compression from 'compression'; import cookieParser from 'cookie-parser'; import express from 'express'; import session from 'express-session'; +import connectRedis from 'connect-redis'; +import Redis from 'ioredis'; import helmet from 'helmet'; import fetch from 'node-fetch'; import morgan from 'morgan'; @@ -34,6 +36,8 @@ import { EXPRESS_SESSION_NAME, EXPRESS_SESSION_PATH, EXPRESS_SESSION_SECRET, + EXPRESS_REDIS_STAND_ALONE_URL, + EXPRESS_REDIS_SENTINEL_CONFIG, EXPRESS_CONTENT_SECURITY_POLICY_CONFIG, } from '../configs/envs'; import { SESSION_IS_EXPIRED, TOKEN_NOT_FOUND, TOKEN_REFRESH_FAILED } from '../constants'; @@ -67,14 +71,48 @@ app.use( crossOriginEmbedderPolicy: false, }), ); -app.use(morgan('combined', { stream: winstonStream })); // send logs to winston +app.use(morgan('combined', { stream: winstonStream })); // send request logs to winston streamer -const FileStore = sessionFileStore(session); -const fileStoreOptions: sessionFileStore.Options = { - path: EXPRESS_SESSION_FILESTORE_PATH || './sessions', - // channel session-file-store warnings to winston - logFn: (message) => winstonLogger.info(message), -}; +let sessionStore: session.Store; + +// use redis session store if redis is available else default to file store +// check for and use a single redis node +if (EXPRESS_REDIS_STAND_ALONE_URL !== undefined) { + const RedisStore = connectRedis(session); + const redisClient = new Redis(EXPRESS_REDIS_STAND_ALONE_URL); + + redisClient.on('connect', () => winstonLogger.info('Redis single node client connected!')); + redisClient.on('reconnecting', () => winstonLogger.info('Redis single node client trying to reconnect')); + redisClient.on('error', (err) => winstonLogger.error('Redis single node client error:', err)); + redisClient.on('end', () => winstonLogger.error('Redis single node client error: Redis client disconnected')); + + sessionStore = new RedisStore({ client: redisClient }); +} +// check for and use redis sentinel if available +else if (EXPRESS_REDIS_SENTINEL_CONFIG !== undefined && Object.keys(EXPRESS_REDIS_SENTINEL_CONFIG).length > 0) { + const RedisStore = connectRedis(session); + + const redisClient = new Redis({ + ...EXPRESS_REDIS_SENTINEL_CONFIG, + }); + + redisClient.on('connect', () => winstonLogger.info('Redis sentinel client connected!')); + redisClient.on('reconnecting', () => winstonLogger.info('Redis sentinel client trying to reconnect')); + redisClient.on('error', (err) => winstonLogger.error('Redis sentinel client error:', err)); + redisClient.on('end', () => winstonLogger.error('Redis sentinel client error: Redis client disconnected')); + + sessionStore = new RedisStore({ client: redisClient }); +} +// else default to file store +else { + winstonLogger.error('Redis Connection Error: Redis configs not provided using file session store'); + + const FileStore = sessionFileStore(session); + sessionStore = new FileStore({ + path: EXPRESS_SESSION_FILESTORE_PATH || './sessions', + logFn: (message) => winstonLogger.info(message), + }); +} let nextPath: string | undefined; @@ -88,7 +126,7 @@ const sess = { resave: true, saveUninitialized: true, secret: EXPRESS_SESSION_SECRET || 'hunter2', - store: new FileStore(fileStoreOptions), + store: sessionStore, }; if (app.get('env') === 'production') { diff --git a/src/app/tests/index.errors.test.ts b/src/app/tests/index.errors.test.ts index af3da58..00656b9 100644 --- a/src/app/tests/index.errors.test.ts +++ b/src/app/tests/index.errors.test.ts @@ -13,16 +13,6 @@ const panic = (err: Error, done: jest.DoneCallback): void => { } }; -// mock out winston logger and stream methods - reduce log noise in test output -jest.mock('../../configs/winston', () => ({ - winstonLogger: { - info: jest.fn(), - error: jest.fn(), - }, - winstonStream: { - write: jest.fn(), - }, -})); jest.mock('client-oauth2', () => { class CodeFlow { private client: ClientOauth2; diff --git a/src/app/tests/index.logging.test.ts b/src/app/tests/index.logging.test.ts index d1414e8..903d20c 100644 --- a/src/app/tests/index.logging.test.ts +++ b/src/app/tests/index.logging.test.ts @@ -17,16 +17,6 @@ const panic = (err: Error, done: jest.DoneCallback): void => { }; jest.mock('../../configs/envs'); -// mock out winston logger and stream methods - reduce log noise in test output -jest.mock('../../configs/winston', () => ({ - winstonLogger: { - info: jest.fn(), - error: jest.fn(), - }, - winstonStream: { - write: jest.fn(), - }, -})); const errorText = 'Token not found'; @@ -116,7 +106,7 @@ describe('src/index.ts', () => { .get('/oauth/state') .then((res) => { // one by winston, other by morgan - expect(logsSpy).toHaveBeenCalledTimes(1); + expect(logsSpy).toHaveBeenCalledTimes(2); expect(logsSpy).toHaveBeenCalledWith('Not authorized'); expect(res.status).toBe(200); expect(res.text).toMatch('Not authorized'); diff --git a/src/app/tests/index.test.ts b/src/app/tests/index.test.ts index 9c57afa..240ca59 100644 --- a/src/app/tests/index.test.ts +++ b/src/app/tests/index.test.ts @@ -4,6 +4,7 @@ import ClientOauth2 from 'client-oauth2'; import nock from 'nock'; import request from 'supertest'; import express from 'express'; +import Redis from 'ioredis'; import { EXPRESS_FRONTEND_OPENSRP_CALLBACK_URL, EXPRESS_SESSION_LOGIN_URL, @@ -29,19 +30,9 @@ const panic = (err: Error, done: jest.DoneCallback): void => { } }; +jest.mock('ioredis', () => jest.requireActual('ioredis-mock')); jest.mock('../../configs/envs'); -// mock out winston logger and stream methods - reduce log noise in test output -jest.mock('../../configs/winston', () => ({ - winstonLogger: { - info: jest.fn(), - error: jest.fn(), - }, - winstonStream: { - write: jest.fn(), - }, -})); jest.mock('node-fetch'); - jest.mock('client-oauth2', () => { class CodeFlow { private client: ClientOauth2; @@ -113,10 +104,14 @@ describe('src/index.ts', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any let cookie: { [key: string]: any }; - afterEach(() => { + afterEach((done) => { JSON.parse = actualJsonParse; jest.resetAllMocks(); jest.clearAllMocks(); + new Redis() + .flushall() + .then(() => done()) + .catch((err) => panic(err, done)); }); it('serves the build.index.html file', (done) => { @@ -430,7 +425,49 @@ describe('src/index.ts', () => { ); expect(winston).toHaveBeenCalledTimes(2); - done(); }); + + it('uses single redis node as session storage', (done) => { + jest.resetModules(); + jest.mock('../../configs/envs', () => ({ + ...jest.requireActual('../../configs/envs'), + EXPRESS_REDIS_STAND_ALONE_URL: 'redis://:@127.0.0.1:1234', + })); + const { default: app2 } = jest.requireActual('../index'); + const { winstonLogger: winstonLogger2 } = jest.requireActual('../../configs/winston'); + const logsSpy = jest.spyOn(winstonLogger2, 'info'); + + request(app2) + .get('/test/endpoint') + .then(() => { + expect(logsSpy).toHaveBeenCalledWith('Redis single node client connected!'); + done(); + }) + .catch((err) => { + panic(err, done); + }); + }); + + it('uses redis sentinel as session storage', (done) => { + jest.resetModules(); + jest.mock('../../configs/envs', () => ({ + ...jest.requireActual('../../configs/envs'), + EXPRESS_REDIS_SENTINEL_CONFIG: + '{"name":"mymaster","sentinels":[{"host":"127.0.0.1","port":26379},{"host":"127.0.0.1","port":6380},{"host":"127.0.0.1","port":6379}]}', + })); + const { default: app2 } = jest.requireActual('../index'); + const { winstonLogger: winstonLogger2 } = jest.requireActual('../../configs/winston'); + const logsSpy = jest.spyOn(winstonLogger2, 'info'); + + request(app2) + .get('/test/endpoint') + .then(() => { + expect(logsSpy).toHaveBeenCalledWith('Redis sentinel client connected!'); + done(); + }) + .catch((err) => { + panic(err, done); + }); + }); }); diff --git a/src/configs/envs.ts b/src/configs/envs.ts index 29be53b..b38b37d 100644 --- a/src/configs/envs.ts +++ b/src/configs/envs.ts @@ -106,3 +106,9 @@ export const EXPRESS_CONTENT_SECURITY_POLICY_CONFIG = JSON.parse( process.env.EXPRESS_CONTENT_SECURITY_POLICY_CONFIG || defaultCsp, ); export type EXPRESS_CONTENT_SECURITY_POLICY_CONFIG = typeof EXPRESS_CONTENT_SECURITY_POLICY_CONFIG; + +// see https://github.com/luin/ioredis#connect-to-redis +export const { EXPRESS_REDIS_STAND_ALONE_URL } = process.env; + +// see https://github.com/luin/ioredis#sentinel +export const EXPRESS_REDIS_SENTINEL_CONFIG = JSON.parse(process.env.EXPRESS_REDIS_SENTINEL_CONFIG || '{}'); diff --git a/src/types/index.ts b/src/types/index.ts index 6b1f681..90c1f25 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,8 +1,14 @@ +/* eslint-disable @typescript-eslint/no-empty-interface */ +/* eslint-disable @typescript-eslint/no-explicit-any */ import 'express-session'; +import 'redis'; declare module 'express-session' { interface Session { - // eslint-disable-next-line @typescript-eslint/no-explicit-any preloadedState?: Record; } } + +declare module 'redis' { + interface RedisClient {} +} diff --git a/tsconfig.json b/tsconfig.json index 0972767..f08cbf5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,8 @@ "paths": { "*": ["node_modules/*"] }, - "strictNullChecks": true + "strictNullChecks": true, + "skipLibCheck": true }, "include": ["src/**/*"], "typeRoots": [ diff --git a/yarn.lock b/yarn.lock index 036e5d8..729bc0c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -343,6 +343,16 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@ioredis/as-callback@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@ioredis/as-callback/-/as-callback-3.0.0.tgz#b96c9b05e6701e85ec6a5e62fa254071b0aec97f" + integrity sha512-Kqv1rZ3WbgOrS+hgzJ5xG5WQuhvzzSTRYvNeyPMLOAM78MHSnuKI20JeJGbpuAt//LCuP0vsexZcorqW7kWhJg== + +"@ioredis/commands@^1.1.1": + version "1.1.1" + resolved "https://registry.yarnpkg.com/@ioredis/commands/-/commands-1.1.1.tgz#2ba4299ea624a6bfac15b35f6df90b0015691ec3" + integrity sha512-fsR4P/ROllzf/7lXYyElUJCheWdTJVJvOTps8v9IWKFATxR61ANOlnoPqhH099xYLrJGpc2ZQ28B3rMeUt5VQg== + "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" @@ -582,6 +592,40 @@ resolved "https://registry.yarnpkg.com/@onaio/session-reducer/-/session-reducer-0.0.13.tgz#96c5749b4c84b47a662ce753a5bec414b565f0df" integrity sha512-A5FdwFyV1Y4NQiyo/aA3g1nlNFsu7rXzR7jGT6J0Nj6ag9xuh6XOG3fyLgBLRbNTB1+Y1g3iFYX8MN89sJrAeQ== +"@redis/bloom@1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@redis/bloom/-/bloom-1.0.2.tgz#42b82ec399a92db05e29fffcdfd9235a5fc15cdf" + integrity sha512-EBw7Ag1hPgFzdznK2PBblc1kdlj5B5Cw3XwI9/oG7tSn85/HKy3X9xHy/8tm/eNXJYHLXHJL/pkwBpFMVVefkw== + +"@redis/client@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@redis/client/-/client-1.1.0.tgz#e52a85aee802796ceb14bf27daf9550f51f238b8" + integrity sha512-xO9JDIgzsZYDl3EvFhl6LC52DP3q3GCMUer8zHgKV6qSYsq1zB+pZs9+T80VgcRogrlRYhi4ZlfX6A+bHiBAgA== + dependencies: + cluster-key-slot "1.1.0" + generic-pool "3.8.2" + yallist "4.0.0" + +"@redis/graph@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@redis/graph/-/graph-1.0.1.tgz#eabc58ba99cd70d0c907169c02b55497e4ec8a99" + integrity sha512-oDE4myMCJOCVKYMygEMWuriBgqlS5FqdWerikMoJxzmmTUErnTRRgmIDa2VcgytACZMFqpAOWDzops4DOlnkfQ== + +"@redis/json@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@redis/json/-/json-1.0.3.tgz#a13fde1d22ebff0ae2805cd8e1e70522b08ea866" + integrity sha512-4X0Qv0BzD9Zlb0edkUoau5c1bInWSICqXAGrpwEltkncUwcxJIGEcVryZhLgb0p/3PkKaLIWkjhHRtLe9yiA7Q== + +"@redis/search@1.0.6": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@redis/search/-/search-1.0.6.tgz#53d7451c2783f011ebc48ec4c2891264e0b22f10" + integrity sha512-pP+ZQRis5P21SD6fjyCeLcQdps+LuTzp2wdUbzxEmNhleighDDTD5ck8+cYof+WLec4csZX7ks+BuoMw0RaZrA== + +"@redis/time-series@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@redis/time-series/-/time-series-1.0.3.tgz#4cfca8e564228c0bddcdf4418cba60c20b224ac4" + integrity sha512-OFp0q4SGrTH0Mruf6oFsHGea58u8vS/iI5+NpYdicaM+7BgqBZH8FFvNZ8rYYLrUO/QRqMq72NpXmxLVNcdmjA== + "@servie/events@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@servie/events/-/events-1.0.0.tgz#8258684b52d418ab7b86533e861186638ecc5dc1" @@ -671,6 +715,16 @@ dependencies: "@types/express" "*" +"@types/connect-redis@^0.0.18": + version "0.0.18" + resolved "https://registry.yarnpkg.com/@types/connect-redis/-/connect-redis-0.0.18.tgz#f2dd17607626a741177efa22641cc2030924eda3" + integrity sha512-iGygGbXgPIr94DEAuoluWhzre3c2/ew5NPlbW9IWvwCTXMM1YCmc7M9wpXMkYqt6kB9aO1sjZnmDzyugUu+2vQ== + dependencies: + "@types/express" "*" + "@types/express-session" "*" + "@types/ioredis" "*" + "@types/redis" "^2.8.0" + "@types/connect@*": version "3.4.35" resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.35.tgz#5fcf6ae445e4021d1fc2219a4873cc73a3bb2ad1" @@ -743,6 +797,20 @@ "@types/react" "*" hoist-non-react-statics "^3.3.0" +"@types/ioredis-mock@^5.6.0": + version "5.6.0" + resolved "https://registry.yarnpkg.com/@types/ioredis-mock/-/ioredis-mock-5.6.0.tgz#78078fedf8323c94234abba0a6b2f10ecbdcb0fe" + integrity sha512-2L20NMYTzNlCeLbi7aXQ/VlFTBu7qYoGefwB0NIDYN5TWzOslzvfl7ttoIN9IVO2LEeY+MBpSWO8oJQklL/o4Q== + dependencies: + "@types/ioredis" "*" + +"@types/ioredis@*": + version "4.28.10" + resolved "https://registry.yarnpkg.com/@types/ioredis/-/ioredis-4.28.10.tgz#40ceb157a4141088d1394bb87c98ed09a75a06ff" + integrity sha512-69LyhUgrXdgcNDv7ogs1qXZomnfOEnSmrmMFqKgt1XMJxmoOSG/u3wYy13yACIfKuMJ8IhKgHafDO3sx19zVQQ== + dependencies: + "@types/node" "*" + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": version "2.0.4" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" @@ -857,6 +925,13 @@ "@types/scheduler" "*" csstype "^3.0.2" +"@types/redis@^2.8.0": + version "2.8.32" + resolved "https://registry.yarnpkg.com/@types/redis/-/redis-2.8.32.tgz#1d3430219afbee10f8cfa389dad2571a05ecfb11" + integrity sha512-7jkMKxcGq9p242exlbsVzuJb57KqHRhNl4dHoQu2Y5v9bCAbtIXXH0R3HleSQW4CTOqpHIYUW3t6tpUj4BVQ+w== + dependencies: + "@types/node" "*" + "@types/request@^2.48.8": version "2.48.8" resolved "https://registry.yarnpkg.com/@types/request/-/request-2.48.8.tgz#0b90fde3b655ab50976cb8c5ac00faca22f5a82c" @@ -1586,6 +1661,11 @@ clone-response@^1.0.2: dependencies: mimic-response "^1.0.0" +cluster-key-slot@1.1.0, cluster-key-slot@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.0.tgz#30474b2a981fb12172695833052bc0d01336d10d" + integrity sha512-2Nii8p3RwAPiFwsnZvukotvow2rIHM+yQ6ZcBXGHdniadkYGZYiGmkHJIbZPIV9nfv7m/U1IPMVVcAhoWFeklw== + co@^4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -1708,6 +1788,11 @@ confusing-browser-globals@^1.0.10: resolved "https://registry.yarnpkg.com/confusing-browser-globals/-/confusing-browser-globals-1.0.11.tgz#ae40e9b57cdd3915408a2805ebd3a5585608dc81" integrity sha512-JsPKdmh8ZkmnHxDk55FZ1TqVLvEQTvoByJZRN9jzI0UjxK/QgAmsphz7PGtqgPieQZ/CQcHWXCR7ATDNhGe+YA== +connect-redis@^6.1.3: + version "6.1.3" + resolved "https://registry.yarnpkg.com/connect-redis/-/connect-redis-6.1.3.tgz#0a83c953f9ece45ae37d304a8e8d1c3c6a60b4b9" + integrity sha512-aaNluLlAn/3JPxRwdzw7lhvEoU6Enb+d83xnokUNhC9dktqBoawKWL+WuxinxvBLTz6q9vReTnUDnUslaz74aw== + content-disposition@0.5.4: version "0.5.4" resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe" @@ -1855,6 +1940,13 @@ debug@^3.2.7: dependencies: ms "^2.1.1" +debug@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + decimal.js@^10.2.1: version "10.3.1" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.3.1.tgz#d8c3a444a9c6774ba60ca6ad7261c3a94fd5e783" @@ -1909,6 +2001,11 @@ delayed-stream@~1.0.0: resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" integrity sha1-3zrhmayt+31ECqrgsp4icrJOxhk= +denque@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/denque/-/denque-2.0.1.tgz#bcef4c1b80dc32efe97515744f21a4229ab8934a" + integrity sha512-tfiWc6BQLXNLpNiR5iGd0Ocu3P3VpxfzFiqubLgMfhfOw9WyvgJBd46CClNn9k3qfbjvT//0cf7AlYRX/OslMQ== + depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" @@ -2488,6 +2585,20 @@ fecha@^4.2.0: resolved "https://registry.yarnpkg.com/fecha/-/fecha-4.2.1.tgz#0a83ad8f86ef62a091e22bb5a039cd03d23eecce" integrity sha512-MMMQ0ludy/nBs1/o0zVOiKTpG7qMbonKUzjJgQFEuvq6INZ1OraKPRAWkBq5vlKLOUMpmNYG1JoN3oDPUQ9m3Q== +fengari-interop@^0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/fengari-interop/-/fengari-interop-0.1.3.tgz#3ad37a90e7430b69b365441e9fc0ba168942a146" + integrity sha512-EtZ+oTu3kEwVJnoymFPBVLIbQcCoy9uWCVnMA6h3M/RqHkUBsLYp29+RRHf9rKr6GwjubWREU1O7RretFIXjHw== + +fengari@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/fengari/-/fengari-0.1.4.tgz#72416693cd9e43bd7d809d7829ddc0578b78b0bb" + integrity sha512-6ujqUuiIYmcgkGz8MGAdERU57EIluGGPSUgGPTsco657EHa+srq0S3/YUl/r9kx1+D+d4rGfYObd+m8K22gB1g== + dependencies: + readline-sync "^1.4.9" + sprintf-js "^1.1.1" + tmp "^0.0.33" + file-entry-cache@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" @@ -2643,6 +2754,11 @@ functional-red-black-tree@^1.0.1: resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= +generic-pool@3.8.2: + version "3.8.2" + resolved "https://registry.yarnpkg.com/generic-pool/-/generic-pool-3.8.2.tgz#aab4f280adb522fdfbdc5e5b64d718d3683f04e9" + integrity sha512-nGToKy6p3PAbYQ7p1UlWl6vSPwfwU6TMSWK7TTu+WUY4ZjyZQGniGGt2oNVvyNSpyZYSB43zMXVLcBm08MTMkg== + gensync@^1.0.0-beta.2: version "1.0.0-beta.2" resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" @@ -3015,6 +3131,31 @@ internal-slot@^1.0.3: has "^1.0.3" side-channel "^1.0.4" +ioredis-mock@^8.2.2: + version "8.2.2" + resolved "https://registry.yarnpkg.com/ioredis-mock/-/ioredis-mock-8.2.2.tgz#9bae98378a396d3ba748fab604ede1b90c53eadf" + integrity sha512-XyJfcF6pqcLHwAYtldkzaLtjRxPw7d8U0FUfjgQ5U/d0vVhFxiXbqsILR4FEOp+ygzyZgBA8xye+uPKu74IH1A== + dependencies: + "@ioredis/as-callback" "^3.0.0" + "@ioredis/commands" "^1.1.1" + fengari "^0.1.4" + fengari-interop "^0.1.3" + +ioredis@^5.0.6: + version "5.0.6" + resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.0.6.tgz#e50b8cc945f1f3ac932b0b8aab4bd8073d1402a9" + integrity sha512-KUm7wPzIet9QrFMoMm09/4bkfVKBUD9KXwBitP3hrNkZ+A6NBndweXGwYIB/7szHcTZgfo7Kvx88SxljJV4D9A== + dependencies: + "@ioredis/commands" "^1.1.1" + cluster-key-slot "^1.1.0" + debug "^4.3.4" + denque "^2.0.1" + lodash.defaults "^4.2.0" + lodash.isarguments "^3.1.0" + redis-errors "^1.2.0" + redis-parser "^3.0.0" + standard-as-callback "^2.1.0" + ip-regex@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" @@ -3920,6 +4061,16 @@ locate-path@^5.0.0: dependencies: p-locate "^4.1.0" +lodash.defaults@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" + integrity sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ== + +lodash.isarguments@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" + integrity sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg== + lodash.isfunction@^3.0.9: version "3.0.9" resolved "https://registry.yarnpkg.com/lodash.isfunction/-/lodash.isfunction-3.0.9.tgz#06de25df4db327ac931981d1bdb067e5af68d051" @@ -4379,6 +4530,11 @@ optionator@^0.9.1: type-check "^0.4.0" word-wrap "^1.2.3" +os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== + p-cancelable@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-1.1.0.tgz#d078d15a3af409220c886f1d9a0ca2e441ab26cc" @@ -4913,6 +5069,35 @@ readdirp@~3.6.0: dependencies: picomatch "^2.2.1" +readline-sync@^1.4.9: + version "1.4.10" + resolved "https://registry.yarnpkg.com/readline-sync/-/readline-sync-1.4.10.tgz#41df7fbb4b6312d673011594145705bf56d8873b" + integrity sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw== + +redis-errors@^1.0.0, redis-errors@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" + integrity sha1-62LSrbFeTq9GEMBK/hUpOEJQq60= + +redis-parser@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4" + integrity sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ= + dependencies: + redis-errors "^1.0.0" + +redis@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/redis/-/redis-4.1.0.tgz#6e400e8edf219e39281afe95e66a3d5f7dcf7289" + integrity sha512-5hvJ8wbzpCCiuN1ges6tx2SAh2XXCY0ayresBmu40/SGusWHFW86TAlIPpbimMX2DFHOX7RN34G2XlPA1Z43zg== + dependencies: + "@redis/bloom" "1.0.2" + "@redis/client" "1.1.0" + "@redis/graph" "1.0.1" + "@redis/json" "1.0.3" + "@redis/search" "1.0.6" + "@redis/time-series" "1.0.3" + redux@^4.0.0, redux@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/redux/-/redux-4.1.2.tgz#140f35426d99bb4729af760afcf79eaaac407104" @@ -5321,6 +5506,11 @@ split-on-first@^1.0.0: resolved "https://registry.yarnpkg.com/split-on-first/-/split-on-first-1.1.0.tgz#f610afeee3b12bce1d0c30425e76398b78249a5f" integrity sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw== +sprintf-js@^1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.2.tgz#da1765262bf8c0f571749f2ad6c26300207ae673" + integrity sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug== + sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -5353,6 +5543,11 @@ stack-utils@^2.0.3: dependencies: escape-string-regexp "^2.0.0" +standard-as-callback@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45" + integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A== + "statuses@>= 1.5.0 < 2", statuses@~1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c" @@ -5586,6 +5781,13 @@ tiny-warning@^1.0.0, tiny-warning@^1.0.3: resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754" integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA== +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + tmpl@1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" @@ -6084,7 +6286,7 @@ y18n@^5.0.5: resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== -yallist@^4.0.0: +yallist@4.0.0, yallist@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==