From b47e228181d3890822dd0acf7a85f85f6507b5b2 Mon Sep 17 00:00:00 2001 From: Martin Lehmann Date: Tue, 2 Nov 2021 15:13:46 +0100 Subject: [PATCH] fix: be explicit when specifying time & replace moment with date-fns (#1072) --- package.json | 1 - src/lib/create-config.ts | 11 +- src/lib/db/client-instance-store.ts | 7 +- src/lib/db/client-metrics-db.ts | 5 +- src/lib/db/client-metrics-store.ts | 5 +- src/lib/db/session-store.ts | 3 +- src/lib/metrics.ts | 16 +-- src/lib/middleware/secure-headers.ts | 3 +- src/lib/middleware/session-db.ts | 7 +- src/lib/routes/admin-api/state.ts | 4 +- src/lib/routes/client-api/feature.test.ts | 5 +- src/lib/services/addon-service.ts | 5 +- src/lib/services/api-token-service.ts | 5 +- .../client-metrics-service-v2.ts | 8 +- .../client-metrics/client-metrics.test.ts | 130 ++++++++++++------ src/lib/services/client-metrics/index.ts | 15 +- src/lib/services/client-metrics/ttl-list.js | 31 +++-- .../services/client-metrics/ttl-list.test.js | 37 ++++- src/lib/services/project-health-service.ts | 11 +- src/lib/services/reset-token-service.ts | 5 +- src/lib/services/version-service.ts | 5 +- src/lib/util/constants.ts | 2 - src/migrator.ts | 11 +- src/test/e2e/api/admin/metrics.e2e.test.ts | 9 +- src/test/e2e/api/client/register.e2e.test.ts | 2 +- .../e2e/services/addon-service.e2e.test.ts | 2 +- .../services/api-token-service.e2e.test.ts | 8 +- .../client-metrics-service.e2e.test.ts | 11 +- .../e2e/services/session-service.e2e.test.ts | 9 +- .../e2e/services/user-service.e2e.test.ts | 5 +- .../client-metrics-store-v2.e2e.test.ts | 13 +- src/test/e2e/stores/event-store.e2e.test.ts | 2 +- yarn.lock | 5 - 33 files changed, 240 insertions(+), 158 deletions(-) diff --git a/package.json b/package.json index 00b149daa0..c279d42167 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,6 @@ "log4js": "^6.0.0", "memoizee": "^0.4.15", "mime": "^2.4.2", - "moment": "^2.24.0", "multer": "^1.4.1", "mustache": "^4.1.0", "node-fetch": "^2.6.1", diff --git a/src/lib/create-config.ts b/src/lib/create-config.ts index 3d34d599c2..ddbe9bbd97 100644 --- a/src/lib/create-config.ts +++ b/src/lib/create-config.ts @@ -19,6 +19,7 @@ import { import { getDefaultLogProvider, LogLevel, validateLogProvider } from './logger'; import { defaultCustomAuthDenyAll } from './default-custom-auth-deny-all'; import { formatBaseUri } from './util/format-base-uri'; +import { minutesToMilliseconds, secondsToMilliseconds } from 'date-fns'; const safeToUpper = (s: string) => (s ? s.toUpperCase() : s); @@ -94,13 +95,13 @@ const defaultDbOptions: IDBOption = { : { rejectUnauthorized: false }, driver: 'postgres', version: process.env.DATABASE_VERSION, - acquireConnectionTimeout: 30000, + acquireConnectionTimeout: secondsToMilliseconds(30), pool: { min: safeNumber(process.env.DATABASE_POOL_MIN, 0), max: safeNumber(process.env.DATABASE_POOL_MAX, 4), idleTimeoutMillis: safeNumber( process.env.DATABASE_POOL_IDLE_TIMEOUT_MS, - 30000, + secondsToMilliseconds(30), ), propagateCreateError: false, }, @@ -120,8 +121,8 @@ const defaultServerOption: IServerOption = { baseUriPath: formatBaseUri(process.env.BASE_URI_PATH), unleashUrl: process.env.UNLEASH_URL || 'http://localhost:4242', serverMetrics: true, - keepAliveTimeout: 60 * 1000, - headersTimeout: 61 * 1000, + keepAliveTimeout: minutesToMilliseconds(1), + headersTimeout: secondsToMilliseconds(61), enableRequestLogger: false, gracefulShutdownEnable: safeBoolean( process.env.GRACEFUL_SHUTDOWN_ENABLE, @@ -129,7 +130,7 @@ const defaultServerOption: IServerOption = { ), gracefulShutdownTimeout: safeNumber( process.env.GRACEFUL_SHUTDOWN_TIMEOUT, - 1000, + secondsToMilliseconds(1), ), secret: process.env.UNLEASH_SECRET || 'super-secret', }; diff --git a/src/lib/db/client-instance-store.ts b/src/lib/db/client-instance-store.ts index c213bb82ed..fc3bcc5a1f 100644 --- a/src/lib/db/client-instance-store.ts +++ b/src/lib/db/client-instance-store.ts @@ -1,12 +1,13 @@ import EventEmitter from 'events'; import { Knex } from 'knex'; import { Logger, LogProvider } from '../logger'; -import Timeout = NodeJS.Timeout; import { IClientInstance, IClientInstanceStore, INewClientInstance, } from '../types/stores/client-instance-store'; +import { hoursToMilliseconds } from 'date-fns'; +import Timeout = NodeJS.Timeout; const metricsHelper = require('../util/metrics-helper'); const { DB_TIME } = require('../metric-events'); @@ -22,8 +23,6 @@ const COLUMNS = [ ]; const TABLE = 'client_instances'; -const ONE_DAY = 24 * 61 * 60 * 1000; - const mapRow = (row) => ({ appName: row.app_name, instanceId: row.instance_id, @@ -65,7 +64,7 @@ export default class ClientInstanceStore implements IClientInstanceStore { }); const clearer = () => this._removeInstancesOlderThanTwoDays(); setTimeout(clearer, 10).unref(); - this.timer = setInterval(clearer, ONE_DAY).unref(); + this.timer = setInterval(clearer, hoursToMilliseconds(24)).unref(); } async _removeInstancesOlderThanTwoDays(): Promise { diff --git a/src/lib/db/client-metrics-db.ts b/src/lib/db/client-metrics-db.ts index a40c85636a..48f2353e23 100644 --- a/src/lib/db/client-metrics-db.ts +++ b/src/lib/db/client-metrics-db.ts @@ -1,12 +1,11 @@ import { Knex } from 'knex'; import { Logger, LogProvider } from '../logger'; import { IClientMetric } from '../types/stores/client-metrics-db'; +import { minutesToMilliseconds } from 'date-fns'; const METRICS_COLUMNS = ['id', 'created_at', 'metrics']; const TABLE = 'client_metrics'; -const ONE_MINUTE = 60 * 1000; - const mapRow = (row) => ({ id: row.id, createdAt: row.created_at, @@ -24,7 +23,7 @@ export class ClientMetricsDb { // Clear old metrics regularly const clearer = () => this.removeMetricsOlderThanOneHour(); setTimeout(clearer, 10).unref(); - this.timer = setInterval(clearer, ONE_MINUTE).unref(); + this.timer = setInterval(clearer, minutesToMilliseconds(1)).unref(); } async removeMetricsOlderThanOneHour(): Promise { diff --git a/src/lib/db/client-metrics-store.ts b/src/lib/db/client-metrics-store.ts index 6e11993491..99fa662c6e 100644 --- a/src/lib/db/client-metrics-store.ts +++ b/src/lib/db/client-metrics-store.ts @@ -5,8 +5,7 @@ import { DB_TIME } from '../metric-events'; import { ClientMetricsDb } from './client-metrics-db'; import { IClientMetric } from '../types/stores/client-metrics-db'; import { IClientMetricsStore } from '../types/stores/client-metrics-store'; - -const TEN_SECONDS = 10 * 1000; +import { secondsToMilliseconds } from 'date-fns'; export class ClientMetricsStore extends EventEmitter @@ -24,7 +23,7 @@ export class ClientMetricsStore private metricsDb: ClientMetricsDb, eventBus: EventEmitter, getLogger: LogProvider, - pollInterval = TEN_SECONDS, + pollInterval = secondsToMilliseconds(10), ) { super(); this.logger = getLogger('client-metrics-store.ts.js'); diff --git a/src/lib/db/session-store.ts b/src/lib/db/session-store.ts index d8bddf8cbd..a7ad0c9e3d 100644 --- a/src/lib/db/session-store.ts +++ b/src/lib/db/session-store.ts @@ -3,6 +3,7 @@ import { Knex } from 'knex'; import { Logger, LogProvider } from '../logger'; import NotFoundError from '../error/notfound-error'; import { ISession, ISessionStore } from '../types/stores/session-store'; +import { addDays } from 'date-fns'; const TABLE = 'unleash_session'; @@ -72,7 +73,7 @@ export default class SessionStore implements ISessionStore { .insert({ sid: data.sid, sess: JSON.stringify(data.sess), - expired: data.expired || new Date(Date.now() + 86400000), + expired: data.expired || addDays(Date.now(), 1), }) .returning(['sid', 'sess', 'created_at', 'expired']); if (row) { diff --git a/src/lib/metrics.ts b/src/lib/metrics.ts index a9f1d8c84e..f0251881b1 100644 --- a/src/lib/metrics.ts +++ b/src/lib/metrics.ts @@ -3,19 +3,17 @@ import EventEmitter from 'events'; import { Knex } from 'knex'; import * as events from './metric-events'; import { - FEATURE_CREATED, - FEATURE_UPDATED, - FEATURE_ARCHIVED, - FEATURE_REVIVED, DB_POOL_UPDATE, + FEATURE_ARCHIVED, + FEATURE_CREATED, + FEATURE_REVIVED, + FEATURE_UPDATED, } from './types/events'; import { IUnleashConfig } from './types/option'; import { IUnleashStores } from './types/stores'; +import { hoursToMilliseconds, minutesToMilliseconds } from 'date-fns'; import Timer = NodeJS.Timer; -const TWO_HOURS = 2 * 60 * 60 * 1000; -const ONE_MINUTE = 60 * 1000; - export default class MetricsMonitor { timer?: Timer; @@ -111,7 +109,7 @@ export default class MetricsMonitor { collectStaticCounters(); this.timer = setInterval( () => collectStaticCounters(), - TWO_HOURS, + hoursToMilliseconds(2), ).unref(); eventBus.on( @@ -199,7 +197,7 @@ export default class MetricsMonitor { this.registerPoolMetrics(db.client.pool, eventBus); this.poolMetricsTimer = setInterval( () => this.registerPoolMetrics(db.client.pool, eventBus), - ONE_MINUTE, + minutesToMilliseconds(1), ); this.poolMetricsTimer.unref(); } diff --git a/src/lib/middleware/secure-headers.ts b/src/lib/middleware/secure-headers.ts index a9119e0654..096e2fb16a 100644 --- a/src/lib/middleware/secure-headers.ts +++ b/src/lib/middleware/secure-headers.ts @@ -1,12 +1,13 @@ import helmet from 'helmet'; import { RequestHandler } from 'express'; import { IUnleashConfig } from '../types/option'; +import { hoursToSeconds } from 'date-fns'; const secureHeaders: (config: IUnleashConfig) => RequestHandler = (config) => { if (config.secureHeaders) { return helmet({ hsts: { - maxAge: 63072000, + maxAge: hoursToSeconds(24 * 365 * 2), // 2 non-leap years includeSubDomains: true, preload: true, }, diff --git a/src/lib/middleware/session-db.ts b/src/lib/middleware/session-db.ts index c70ad36456..01527006ff 100644 --- a/src/lib/middleware/session-db.ts +++ b/src/lib/middleware/session-db.ts @@ -3,16 +3,16 @@ import session from 'express-session'; import knexSessionStore from 'connect-session-knex'; import { RequestHandler } from 'express'; import { IUnleashConfig } from '../types/option'; +import { hoursToMilliseconds } from 'date-fns'; -const TWO_DAYS = 48 * 60 * 60 * 1000; -const HOUR = 60 * 60 * 1000; function sessionDb( config: Pick, knex: Knex, ): RequestHandler { let store; const { db } = config.session; - const age = config.session.ttlHours * HOUR || TWO_DAYS; + const age = + hoursToMilliseconds(config.session.ttlHours) || hoursToMilliseconds(48); const KnexSessionStore = knexSessionStore(session); if (db) { store = new KnexSessionStore({ @@ -41,4 +41,5 @@ function sessionDb( }, }); } + export default sessionDb; diff --git a/src/lib/routes/admin-api/state.ts b/src/lib/routes/admin-api/state.ts index 2ff57e322e..a57f19e739 100644 --- a/src/lib/routes/admin-api/state.ts +++ b/src/lib/routes/admin-api/state.ts @@ -1,7 +1,7 @@ import * as mime from 'mime'; import YAML from 'js-yaml'; -import moment from 'moment'; import multer from 'multer'; +import { format as formatDate } from 'date-fns'; import { Request, Response } from 'express'; import Controller from '../controller'; import { ADMIN } from '../../types/permissions'; @@ -88,7 +88,7 @@ class StateController extends Controller { includeTags, includeEnvironments, }); - const timestamp = moment().format('YYYY-MM-DD_HH-mm-ss'); + const timestamp = formatDate(Date.now(), 'yyyy-MM-dd_HH-mm-ss'); if (format === 'yaml') { if (downloadFile) { res.attachment(`export-${timestamp}.yml`); diff --git a/src/lib/routes/client-api/feature.test.ts b/src/lib/routes/client-api/feature.test.ts index 5b19b7fcd3..9f814399b9 100644 --- a/src/lib/routes/client-api/feature.test.ts +++ b/src/lib/routes/client-api/feature.test.ts @@ -6,6 +6,7 @@ import getApp from '../../app'; import { createServices } from '../../services'; import FeatureController from './feature'; import { createTestConfig } from '../../../test/config/test-config'; +import { secondsToMilliseconds } from 'date-fns'; const eventBus = new EventEmitter(); @@ -74,7 +75,7 @@ test('if caching is enabled should memoize', async () => { experimental: { clientFeatureMemoize: { enabled: true, - maxAge: 10000, + maxAge: secondsToMilliseconds(10), }, }, }, @@ -100,7 +101,7 @@ test('if caching is not enabled all calls goes to service', async () => { experimental: { clientFeatureMemoize: { enabled: false, - maxAge: 10000, + maxAge: secondsToMilliseconds(10), }, }, }, diff --git a/src/lib/services/addon-service.ts b/src/lib/services/addon-service.ts index f7251eef91..dc851f1e1a 100644 --- a/src/lib/services/addon-service.ts +++ b/src/lib/services/addon-service.ts @@ -12,11 +12,10 @@ import { IAddon, IAddonDto, IAddonStore } from '../types/stores/addon-store'; import { IUnleashStores } from '../types/stores'; import { IUnleashConfig } from '../types/option'; import { IAddonDefinition } from '../types/model'; +import { minutesToMilliseconds } from 'date-fns'; const SUPPORTED_EVENTS = Object.keys(events).map((k) => events[k]); -const ADDONS_CACHE_TIME = 60 * 1000; // 60s - const MASKED_VALUE = '*****'; interface ISensitiveParams { @@ -75,7 +74,7 @@ export default class AddonService { async () => addonStore.getAll({ enabled: true }), { promise: true, - maxAge: ADDONS_CACHE_TIME, + maxAge: minutesToMilliseconds(1), }, ); } diff --git a/src/lib/services/api-token-service.ts b/src/lib/services/api-token-service.ts index 6d69657b0f..23bc5c0a96 100644 --- a/src/lib/services/api-token-service.ts +++ b/src/lib/services/api-token-service.ts @@ -13,8 +13,7 @@ import { import { IApiTokenStore } from '../types/stores/api-token-store'; import { FOREIGN_KEY_VIOLATION } from '../error/db-error'; import BadDataError from '../error/bad-data-error'; - -const ONE_MINUTE = 60_000; +import { minutesToMilliseconds } from 'date-fns'; export class ApiTokenService { private store: IApiTokenStore; @@ -34,7 +33,7 @@ export class ApiTokenService { this.fetchActiveTokens(); this.timer = setInterval( () => this.fetchActiveTokens(), - ONE_MINUTE, + minutesToMilliseconds(1), ).unref(); } diff --git a/src/lib/services/client-metrics/client-metrics-service-v2.ts b/src/lib/services/client-metrics/client-metrics-service-v2.ts index 25248c49fe..18120f3143 100644 --- a/src/lib/services/client-metrics/client-metrics-service-v2.ts +++ b/src/lib/services/client-metrics/client-metrics-service-v2.ts @@ -8,9 +8,7 @@ import { IClientMetricsStoreV2, } from '../../types/stores/client-metrics-store-v2'; import { clientMetricsSchema } from './client-metrics-schema'; - -const FIVE_MINUTES = 5 * 60 * 1000; -const ONE_DAY = 24 * 60 * 60 * 1000; +import { hoursToMilliseconds, minutesToMilliseconds } from 'date-fns'; export default class ClientMetricsServiceV2 { private timer: NodeJS.Timeout; @@ -24,7 +22,7 @@ export default class ClientMetricsServiceV2 { constructor( { clientMetricsStoreV2 }: Pick, { getLogger }: Pick, - bulkInterval = FIVE_MINUTES, + bulkInterval = minutesToMilliseconds(5), ) { this.clientMetricsStoreV2 = clientMetricsStoreV2; @@ -33,7 +31,7 @@ export default class ClientMetricsServiceV2 { this.bulkInterval = bulkInterval; this.timer = setInterval(() => { this.clientMetricsStoreV2.clearMetrics(48); - }, ONE_DAY); + }, hoursToMilliseconds(24)); this.timer.unref(); } diff --git a/src/lib/services/client-metrics/client-metrics.test.ts b/src/lib/services/client-metrics/client-metrics.test.ts index e0c711f8cb..34dec46f8a 100644 --- a/src/lib/services/client-metrics/client-metrics.test.ts +++ b/src/lib/services/client-metrics/client-metrics.test.ts @@ -1,8 +1,51 @@ import EventEmitter from 'events'; -import moment from 'moment'; import ClientMetricsService from './index'; import getLogger from '../../../test/fixtures/no-logger'; import { IClientApp } from '../../types/model'; +import { + addHours, + addMinutes, + hoursToMilliseconds, + minutesToMilliseconds, + secondsToMilliseconds, + subHours, + subMinutes, + subSeconds, +} from 'date-fns'; + +/** + * A utility to wait for any pending promises in the test subject code. + * For instance, if the test needs to wait for a timeout/interval handler, + * and that handler does something async, advancing the timers is not enough: + * We have to explicitly wait for the second promise. + * For more info, see https://stackoverflow.com/a/51045733/2868829 + * + * Usage in test code after advancing timers, but before making assertions: + * + * test('hello', async () => { + * jest.useFakeTimers('modern'); + * + * // Schedule a timeout with a callback that does something async + * // before calling our spy + * const spy = jest.fn(); + * setTimeout(async () => { + * await Promise.resolve(); + * spy(); + * }, 1000); + * + * expect(spy).not.toHaveBeenCalled(); + * + * jest.advanceTimersByTime(1500); + * await flushPromises(); // this is required to make it work! + * + * expect(spy).toHaveBeenCalledTimes(1); + * + * jest.useRealTimers(); + * }); + */ +function flushPromises() { + return Promise.resolve(setImmediate); +} const appName = 'appName'; const instanceId = 'instanceId'; @@ -40,8 +83,8 @@ test('data should expire', () => { appName, instanceId, bucket: { - start: Date.now() - 2000, - stop: Date.now() - 1000, + start: subSeconds(Date.now(), 2), + stop: subSeconds(Date.now(), 1), toggles: { toggleX: { yes: 123, @@ -61,11 +104,11 @@ test('data should expire', () => { lastMinExpires++; }); - jest.advanceTimersByTime(60 * 1000); + jest.advanceTimersByTime(minutesToMilliseconds(1)); expect(lastMinExpires).toBe(1); expect(lastHourExpires).toBe(0); - jest.advanceTimersByTime(60 * 60 * 1000); + jest.advanceTimersByTime(hoursToMilliseconds(1)); expect(lastMinExpires).toBe(1); expect(lastHourExpires).toBe(1); @@ -201,36 +244,36 @@ test('should have correct values for lastMinute', () => { const now = new Date(); const input = [ { - start: moment(now).subtract(1, 'hour'), - stop: moment(now).subtract(59, 'minutes'), + start: subHours(now, 1), + stop: subMinutes(now, 59), toggles: { toggle: { yes: 10, no: 10 }, }, }, { - start: moment(now).subtract(30, 'minutes'), - stop: moment(now).subtract(29, 'minutes'), + start: subMinutes(now, 30), + stop: subMinutes(now, 29), toggles: { toggle: { yes: 10, no: 10 }, }, }, { - start: moment(now).subtract(2, 'minutes'), - stop: moment(now).subtract(1, 'minutes'), + start: subMinutes(now, 2), + stop: subMinutes(now, 1), toggles: { toggle: { yes: 10, no: 10 }, }, }, { - start: moment(now).subtract(2, 'minutes'), - stop: moment(now).subtract(59, 'seconds'), + start: subMinutes(now, 2), + stop: subSeconds(now, 59), toggles: { toggle: { yes: 10, no: 10 }, }, }, { - start: moment(now), - stop: moment(now).subtract(30, 'seconds'), + start: now, + stop: subSeconds(now, 30), toggles: { toggle: { yes: 10, no: 10 }, }, @@ -252,11 +295,11 @@ test('should have correct values for lastMinute', () => { let c = metrics.getTogglesMetrics(); expect(c.lastMinute.toggle).toEqual({ yes: 20, no: 20 }); - jest.advanceTimersByTime(10 * 1000); + jest.advanceTimersByTime(10_000); c = metrics.getTogglesMetrics(); expect(c.lastMinute.toggle).toEqual({ yes: 10, no: 10 }); - jest.advanceTimersByTime(20 * 1000); + jest.advanceTimersByTime(20_000); c = metrics.getTogglesMetrics(); expect(c.lastMinute.toggle).toEqual({ yes: 0, no: 0 }); @@ -270,32 +313,32 @@ test('should have correct values for lastHour', () => { const clientMetricsStore = new EventEmitter(); const metrics = createMetricsService(clientMetricsStore); - const now = new Date(); + const now = Date.now(); const input = [ { - start: moment(now).subtract(1, 'hour'), - stop: moment(now).subtract(59, 'minutes'), + start: subHours(now, 1), + stop: subMinutes(now, 59), toggles: { toggle: { yes: 10, no: 10 }, }, }, { - start: moment(now).subtract(30, 'minutes'), - stop: moment(now).subtract(29, 'minutes'), + start: subMinutes(now, 30), + stop: subMinutes(now, 29), toggles: { toggle: { yes: 10, no: 10 }, }, }, { - start: moment(now).subtract(15, 'minutes'), - stop: moment(now).subtract(14, 'minutes'), + start: subMinutes(now, 15), + stop: subMinutes(now, 14), toggles: { toggle: { yes: 10, no: 10 }, }, }, { - start: moment(now).add(59, 'minutes'), - stop: moment(now).add(1, 'hour'), + start: addMinutes(now, 59), + stop: addHours(now, 1), toggles: { toggle: { yes: 11, no: 11 }, }, @@ -318,27 +361,27 @@ test('should have correct values for lastHour', () => { let c = metrics.getTogglesMetrics(); expect(c.lastHour.toggle).toEqual({ yes: 41, no: 41 }); - jest.advanceTimersByTime(10 * 1000); + jest.advanceTimersByTime(10_000); c = metrics.getTogglesMetrics(); expect(c.lastHour.toggle).toEqual({ yes: 41, no: 41 }); // at 30 - jest.advanceTimersByTime(30 * 60 * 1000); + jest.advanceTimersByTime(minutesToMilliseconds(30)); c = metrics.getTogglesMetrics(); expect(c.lastHour.toggle).toEqual({ yes: 31, no: 31 }); // at 45 - jest.advanceTimersByTime(15 * 60 * 1000); + jest.advanceTimersByTime(minutesToMilliseconds(15)); c = metrics.getTogglesMetrics(); expect(c.lastHour.toggle).toEqual({ yes: 21, no: 21 }); // at 1:15 - jest.advanceTimersByTime(30 * 60 * 1000); + jest.advanceTimersByTime(minutesToMilliseconds(30)); c = metrics.getTogglesMetrics(); expect(c.lastHour.toggle).toEqual({ yes: 11, no: 11 }); // at 2:00 - jest.advanceTimersByTime(45 * 60 * 1000); + jest.advanceTimersByTime(minutesToMilliseconds(45)); c = metrics.getTogglesMetrics(); expect(c.lastHour.toggle).toEqual({ yes: 0, no: 0 }); @@ -421,7 +464,8 @@ test('Multiple registrations of same appname and instanceid within same time per await clientMetrics.registerClient(client1, '127.0.0.1'); await clientMetrics.registerClient(client1, '127.0.0.1'); await clientMetrics.registerClient(client1, '127.0.0.1'); - await jest.advanceTimersByTime(7 * 1000); + jest.advanceTimersByTime(7000); + await flushPromises(); expect(appStoreSpy).toHaveBeenCalledTimes(1); expect(bulkSpy).toHaveBeenCalledTimes(1); @@ -448,6 +492,7 @@ test('Multiple unique clients causes multiple registrations', async () => { const clientInstanceStore: any = { bulkUpsert: bulkSpy, }; + const clientMetrics = new ClientMetricsService( { clientMetricsStore, @@ -479,10 +524,9 @@ test('Multiple unique clients causes multiple registrations', async () => { await clientMetrics.registerClient(client2, '127.0.0.1'); await clientMetrics.registerClient(client2, '127.0.0.1'); await clientMetrics.registerClient(client2, '127.0.0.1'); - await jest.advanceTimersByTime(7 * 1000); - expect(appStoreSpy).toHaveBeenCalledTimes(1); - expect(bulkSpy).toHaveBeenCalledTimes(1); + jest.advanceTimersByTime(7000); + await flushPromises(); const registrations = appStoreSpy.mock.calls[0][0]; @@ -501,7 +545,7 @@ test('Same client registered outside of dedup interval will be registered twice' bulkUpsert: bulkSpy, }; - const bulkInterval = 2000; + const bulkInterval = secondsToMilliseconds(2); const clientMetrics = new ClientMetricsService( { @@ -525,11 +569,16 @@ test('Same client registered outside of dedup interval will be registered twice' await clientMetrics.registerClient(client1, '127.0.0.1'); await clientMetrics.registerClient(client1, '127.0.0.1'); await clientMetrics.registerClient(client1, '127.0.0.1'); - await jest.advanceTimersByTime(3 * 1000); + + jest.advanceTimersByTime(3000); + await clientMetrics.registerClient(client1, '127.0.0.1'); await clientMetrics.registerClient(client1, '127.0.0.1'); await clientMetrics.registerClient(client1, '127.0.0.1'); - await jest.advanceTimersByTime(3 * 1000); + + jest.advanceTimersByTime(3000); + await flushPromises(); + expect(appStoreSpy).toHaveBeenCalledTimes(2); expect(bulkSpy).toHaveBeenCalledTimes(2); @@ -552,8 +601,7 @@ test('No registrations during a time period will not call stores', async () => { const clientInstanceStore: any = { bulkUpsert: bulkSpy, }; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const clientMetrics = new ClientMetricsService( + new ClientMetricsService( { clientMetricsStore, strategyStore: null, @@ -564,7 +612,7 @@ test('No registrations during a time period will not call stores', async () => { }, { getLogger }, ); - await jest.advanceTimersByTime(6 * 1000); + jest.advanceTimersByTime(6000); expect(appStoreSpy).toHaveBeenCalledTimes(0); expect(bulkSpy).toHaveBeenCalledTimes(0); jest.useRealTimers(); diff --git a/src/lib/services/client-metrics/index.ts b/src/lib/services/client-metrics/index.ts index f853667985..436f1e7008 100644 --- a/src/lib/services/client-metrics/index.ts +++ b/src/lib/services/client-metrics/index.ts @@ -21,12 +21,9 @@ import { IMetricCounts, IMetricsBucket, } from '../../types/model'; - -import TTLList = require('./ttl-list'); import { clientRegisterSchema } from './register-schema'; - -const FIVE_SECONDS = 5 * 1000; -const FIVE_MINUTES = 5 * 60 * 1000; +import { minutesToMilliseconds, secondsToMilliseconds } from 'date-fns'; +import TTLList = require('./ttl-list'); export default class ClientMetricsService { globalCount = 0; @@ -38,13 +35,13 @@ export default class ClientMetricsService { lastMinuteProjection = new Projection(); lastHourList = new TTLList({ - interval: 10000, + interval: secondsToMilliseconds(10), }); logger = null; lastMinuteList = new TTLList({ - interval: 10000, + interval: secondsToMilliseconds(10), expireType: 'minutes', expireAmount: 1, }); @@ -87,8 +84,8 @@ export default class ClientMetricsService { | 'eventStore' >, { getLogger }: Pick, - bulkInterval = FIVE_SECONDS, - announcementInterval = FIVE_MINUTES, + bulkInterval = secondsToMilliseconds(5), + announcementInterval = minutesToMilliseconds(5), ) { this.clientMetricsStore = clientMetricsStore; this.strategyStore = strategyStore; diff --git a/src/lib/services/client-metrics/ttl-list.js b/src/lib/services/client-metrics/ttl-list.js index c996f92f65..e81538cb30 100644 --- a/src/lib/services/client-metrics/ttl-list.js +++ b/src/lib/services/client-metrics/ttl-list.js @@ -1,13 +1,18 @@ 'use strict'; const { EventEmitter } = require('events'); -const moment = require('moment'); const List = require('./list'); +const { + add, + isFuture, + addMilliseconds, + secondsToMilliseconds, +} = require('date-fns'); // this list must have entries with sorted ttl range module.exports = class TTLList extends EventEmitter { constructor({ - interval = 1000, + interval = secondsToMilliseconds(1), expireAmount = 1, expireType = 'hours', } = {}) { @@ -16,6 +21,14 @@ module.exports = class TTLList extends EventEmitter { this.expireAmount = expireAmount; this.expireType = expireType; + this.getExpiryFrom = (timestamp) => { + if (this.expireType === 'milliseconds') { + return addMilliseconds(timestamp, expireAmount); + } else { + return add(timestamp, { [expireType]: expireAmount }); + } + }; + this.list = new List(); this.list.on('evicted', ({ value, ttl }) => { @@ -36,8 +49,8 @@ module.exports = class TTLList extends EventEmitter { } add(value, timestamp = new Date()) { - const ttl = moment(timestamp).add(this.expireAmount, this.expireType); - if (moment().isBefore(ttl)) { + const ttl = this.getExpiryFrom(timestamp); + if (isFuture(ttl)) { this.list.add({ ttl, value }); } else { this.emit('expire', value, ttl); @@ -45,17 +58,13 @@ module.exports = class TTLList extends EventEmitter { } timedCheck() { - const now = moment(); - this.list.reverseRemoveUntilTrue(({ value }) => - now.isBefore(value.ttl), - ); + this.list.reverseRemoveUntilTrue(({ value }) => isFuture(value.ttl)); this.startTimer(); } destroy() { - // https://github.com/nodejs/node/issues/9561 - // clearTimeout(this.timer); - // this.timer = null; + clearTimeout(this.timer); + this.timer = null; this.list = null; } }; diff --git a/src/lib/services/client-metrics/ttl-list.test.js b/src/lib/services/client-metrics/ttl-list.test.js index c45bce20e3..fc4d39d20b 100644 --- a/src/lib/services/client-metrics/ttl-list.test.js +++ b/src/lib/services/client-metrics/ttl-list.test.js @@ -1,7 +1,7 @@ 'use strict'; -const moment = require('moment'); const TTLList = require('./ttl-list'); +const { addMilliseconds } = require('date-fns'); test('should emit expire', (done) => { jest.useFakeTimers('modern'); @@ -31,10 +31,10 @@ test('should slice off list', () => { expireType: 'milliseconds', }); - list.add({ n: '1' }, moment().add(1, 'milliseconds')); - list.add({ n: '2' }, moment().add(50, 'milliseconds')); - list.add({ n: '3' }, moment().add(200, 'milliseconds')); - list.add({ n: '4' }, moment().add(300, 'milliseconds')); + list.add({ n: '1' }, addMilliseconds(Date.now(), 1)); + list.add({ n: '2' }, addMilliseconds(Date.now(), 50)); + list.add({ n: '3' }, addMilliseconds(Date.now(), 200)); + list.add({ n: '4' }, addMilliseconds(Date.now(), 300)); const expired = []; @@ -43,18 +43,45 @@ test('should slice off list', () => { expired.push(entry); }); + expect(expired).toHaveLength(0); + expect(list.list.toArray()).toHaveLength(4); + jest.advanceTimersByTime(21); expect(expired).toHaveLength(1); + expect(list.list.toArray()).toHaveLength(3); jest.advanceTimersByTime(51); expect(expired).toHaveLength(2); + expect(list.list.toArray()).toHaveLength(2); jest.advanceTimersByTime(201); expect(expired).toHaveLength(3); + expect(list.list.toArray()).toHaveLength(1); jest.advanceTimersByTime(301); expect(expired).toHaveLength(4); + expect(list.list.toArray()).toHaveLength(0); list.destroy(); jest.useRealTimers(); }); + +test('should add item created in the past but expiring in the future', () => { + jest.useFakeTimers('modern'); + + const list = new TTLList({ + interval: 10, + expireAmount: 10, + expireType: 'milliseconds', + }); + + const expireCallback = jest.fn(); + list.on('expire', expireCallback); + + list.add({ n: '1' }, new Date()); + + expect(expireCallback).not.toHaveBeenCalled(); + expect(list.list.toArray()).toHaveLength(1); + + jest.useRealTimers(); +}); diff --git a/src/lib/services/project-health-service.ts b/src/lib/services/project-health-service.ts index 5d6599de18..ec2639b608 100644 --- a/src/lib/services/project-health-service.ts +++ b/src/lib/services/project-health-service.ts @@ -8,15 +8,12 @@ import { IProjectHealthReport, IProjectOverview, } from '../types/model'; -import { - MILLISECONDS_IN_DAY, - MILLISECONDS_IN_ONE_HOUR, -} from '../util/constants'; import { IFeatureToggleStore } from '../types/stores/feature-toggle-store'; import { IFeatureTypeStore } from '../types/stores/feature-type-store'; import { IProjectStore } from '../types/stores/project-store'; -import Timer = NodeJS.Timer; import FeatureToggleServiceV2 from './feature-toggle-service-v2'; +import { hoursToMilliseconds } from 'date-fns'; +import Timer = NodeJS.Timer; export default class ProjectHealthService { private logger: Logger; @@ -52,7 +49,7 @@ export default class ProjectHealthService { this.featureTypes = new Map(); this.healthRatingTimer = setInterval( () => this.setHealthRating(), - MILLISECONDS_IN_ONE_HOUR, + hoursToMilliseconds(1), ).unref(); this.featureToggleService = featureToggleService; } @@ -116,7 +113,7 @@ export default class ProjectHealthService { ); return ( !feature.stale && - diff >= featureTypeExpectedLifetime * MILLISECONDS_IN_DAY + diff >= featureTypeExpectedLifetime * hoursToMilliseconds(24) ); }).length; } diff --git a/src/lib/services/reset-token-service.ts b/src/lib/services/reset-token-service.ts index 3a272fa590..ffab4ea337 100644 --- a/src/lib/services/reset-token-service.ts +++ b/src/lib/services/reset-token-service.ts @@ -11,8 +11,7 @@ import { IResetToken, IResetTokenStore, } from '../types/stores/reset-token-store'; - -const ONE_DAY = 86_400_000; +import { hoursToMilliseconds } from 'date-fns'; interface IInviteLinks { [key: string]: string; @@ -106,7 +105,7 @@ export default class ResetTokenService { async createToken( tokenUser: number, creator: string, - expiryDelta: number = ONE_DAY, + expiryDelta: number = hoursToMilliseconds(24), ): Promise { const token = await this.generateToken(); const expiry = new Date(Date.now() + expiryDelta); diff --git a/src/lib/services/version-service.ts b/src/lib/services/version-service.ts index 5407f4f2cd..8fa47abd81 100644 --- a/src/lib/services/version-service.ts +++ b/src/lib/services/version-service.ts @@ -4,8 +4,7 @@ import { IUnleashConfig } from '../types/option'; import version from '../util/version'; import { Logger } from '../logger'; import { ISettingStore } from '../types/stores/settings-store'; - -const TWO_DAYS = 48 * 60 * 60 * 1000; +import { hoursToMilliseconds } from 'date-fns'; export interface IVersionInfo { oss: string; @@ -66,7 +65,7 @@ export default class VersionService { await this.checkLatestVersion(); this.timer = setInterval( async () => this.checkLatestVersion(), - TWO_DAYS, + hoursToMilliseconds(48), ); this.timer.unref(); } diff --git a/src/lib/util/constants.ts b/src/lib/util/constants.ts index 6930597ae8..c46e2644b8 100644 --- a/src/lib/util/constants.ts +++ b/src/lib/util/constants.ts @@ -1,3 +1 @@ -export const MILLISECONDS_IN_DAY = 86400000; -export const MILLISECONDS_IN_ONE_HOUR = 3600000; export const DEFAULT_ENV = 'default'; diff --git a/src/migrator.ts b/src/migrator.ts index b119668b2e..694759b8b8 100644 --- a/src/migrator.ts +++ b/src/migrator.ts @@ -1,11 +1,15 @@ import { log } from 'db-migrate-shared'; import { getInstance } from 'db-migrate'; import { IUnleashConfig } from './lib/types/option'; +import { secondsToMilliseconds } from 'date-fns'; log.setLogLevel('error'); export async function migrateDb({ db }: IUnleashConfig): Promise { - const custom = { ...db, connectionTimeoutMillis: 10000 }; + const custom = { + ...db, + connectionTimeoutMillis: secondsToMilliseconds(10), + }; const dbm = getInstance(true, { cwd: __dirname, @@ -18,7 +22,10 @@ export async function migrateDb({ db }: IUnleashConfig): Promise { // This exists to ease testing export async function resetDb({ db }: IUnleashConfig): Promise { - const custom = { ...db, connectionTimeoutMillis: 10000 }; + const custom = { + ...db, + connectionTimeoutMillis: secondsToMilliseconds(10), + }; const dbm = getInstance(true, { cwd: __dirname, diff --git a/src/test/e2e/api/admin/metrics.e2e.test.ts b/src/test/e2e/api/admin/metrics.e2e.test.ts index 3f39dbcfb9..a61c3d409f 100644 --- a/src/test/e2e/api/admin/metrics.e2e.test.ts +++ b/src/test/e2e/api/admin/metrics.e2e.test.ts @@ -1,6 +1,7 @@ import dbInit from '../../helpers/database-init'; import { setupApp } from '../../helpers/test-helper'; import getLogger from '../../../fixtures/no-logger'; +import { parseISO } from 'date-fns'; let app; let db; @@ -28,25 +29,27 @@ beforeEach(async () => { description: 'Some desc', announced: true, }); + + const clientStartedDate = parseISO('2018-01-15T14:35:38.494Z'); await db.stores.clientInstanceStore.insert({ appName: 'demo-app-1', instanceId: 'test-1', strategies: ['default'], - started: 1516026938494, + started: clientStartedDate, interval: 10, }); await db.stores.clientInstanceStore.insert({ appName: 'demo-seed-2', instanceId: 'test-2', strategies: ['default'], - started: 1516026938494, + started: clientStartedDate, interval: 10, }); await db.stores.clientInstanceStore.insert({ appName: 'deletable-app', instanceId: 'inst-1', strategies: ['default'], - started: 1516026938494, + started: clientStartedDate, interval: 10, }); await app.services.clientMetricsService.addPayload({ diff --git a/src/test/e2e/api/client/register.e2e.test.ts b/src/test/e2e/api/client/register.e2e.test.ts index 59d4d8f2c2..2f434b29fe 100644 --- a/src/test/e2e/api/client/register.e2e.test.ts +++ b/src/test/e2e/api/client/register.e2e.test.ts @@ -60,7 +60,7 @@ test('should allow client to register multiple times', async () => { .send(clientRegistration) .expect(202); - jest.advanceTimersByTime(6 * 1000); + jest.advanceTimersByTime(6000); expect(clientApplicationsStore.exists(clientRegistration)).toBeTruthy(); expect(clientInstanceStore.exists(clientRegistration)).toBeTruthy(); jest.useRealTimers(); diff --git a/src/test/e2e/services/addon-service.e2e.test.ts b/src/test/e2e/services/addon-service.e2e.test.ts index c73fd27748..eea3e46ed8 100644 --- a/src/test/e2e/services/addon-service.e2e.test.ts +++ b/src/test/e2e/services/addon-service.e2e.test.ts @@ -78,7 +78,7 @@ test('should only return active addons', async () => { await addonService.createAddon(config2, 'me@mail.com'); await addonService.createAddon(config3, 'me@mail.com'); - jest.advanceTimersByTime(61 * 1000); + jest.advanceTimersByTime(61_000); const activeAddons = await addonService.fetchAddonConfigs(); const allAddons = await addonService.getAddons(); diff --git a/src/test/e2e/services/api-token-service.e2e.test.ts b/src/test/e2e/services/api-token-service.e2e.test.ts index bf20c59186..55cb140a8e 100644 --- a/src/test/e2e/services/api-token-service.e2e.test.ts +++ b/src/test/e2e/services/api-token-service.e2e.test.ts @@ -4,6 +4,7 @@ import { ApiTokenService } from '../../../lib/services/api-token-service'; import { createTestConfig } from '../../config/test-config'; import { ApiTokenType, IApiToken } from '../../../lib/types/models/api-token'; import { DEFAULT_ENV } from '../../../lib/util/constants'; +import { addDays, subDays } from 'date-fns'; let db; let stores; @@ -102,13 +103,14 @@ test('should update expiry of token', async () => { }); test('should only return valid tokens', async () => { - const today = new Date(); - const tomorrow = new Date(today.getTime() + 24 * 60 * 60 * 1000); + const now = Date.now(); + const yesterday = subDays(now, 1); + const tomorrow = addDays(now, 1); await apiTokenService.createApiToken({ username: 'default-expired', type: ApiTokenType.CLIENT, - expiresAt: new Date('2021-01-01'), + expiresAt: yesterday, project: '*', environment: DEFAULT_ENV, }); diff --git a/src/test/e2e/services/client-metrics-service.e2e.test.ts b/src/test/e2e/services/client-metrics-service.e2e.test.ts index e45fd51599..2f61dfbfce 100644 --- a/src/test/e2e/services/client-metrics-service.e2e.test.ts +++ b/src/test/e2e/services/client-metrics-service.e2e.test.ts @@ -1,5 +1,6 @@ import ClientMetricsService from '../../../lib/services/client-metrics'; import { IClientApp } from '../../../lib/types/model'; +import { secondsToMilliseconds } from 'date-fns'; const faker = require('faker'); const dbInit = require('../helpers/database-init'); @@ -13,11 +14,15 @@ let clientMetricsService; beforeAll(async () => { db = await dbInit('client_metrics_service_serial', getLogger); stores = db.stores; + + const bulkInterval = secondsToMilliseconds(0.5); + const announcementInterval = secondsToMilliseconds(2); + clientMetricsService = new ClientMetricsService( stores, { getLogger }, - 500, - 2000, + bulkInterval, + announcementInterval, ); }); @@ -53,7 +58,7 @@ test('Apps registered should be announced', async () => { const first = await stores.clientApplicationsStore.getUnannounced(); expect(first.length).toBe(2); await clientMetricsService.registerClient(clientRegistration, '127.0.0.1'); - await new Promise((res) => setTimeout(res, 2000)); + await new Promise((res) => setTimeout(res, secondsToMilliseconds(2))); const second = await stores.clientApplicationsStore.getUnannounced(); expect(second.length).toBe(0); const events = await stores.eventStore.getEvents(); diff --git a/src/test/e2e/services/session-service.e2e.test.ts b/src/test/e2e/services/session-service.e2e.test.ts index e974939703..8e273b72ff 100644 --- a/src/test/e2e/services/session-service.e2e.test.ts +++ b/src/test/e2e/services/session-service.e2e.test.ts @@ -2,6 +2,7 @@ import noLoggerProvider from '../../fixtures/no-logger'; import dbInit from '../helpers/database-init'; import SessionService from '../../../lib/services/session-service'; import NotFoundError from '../../../lib/error/notfound-error'; +import { addDays, minutesToMilliseconds } from 'date-fns'; let stores; let db; @@ -10,8 +11,8 @@ const newSession = { sid: 'abc123', sess: { cookie: { - originalMaxAge: 2880000, - expires: new Date(Date.now() + 86_400_000).toDateString(), + originalMaxAge: minutesToMilliseconds(48), + expires: addDays(Date.now(), 1).toDateString(), secure: false, httpOnly: true, path: '/', @@ -31,8 +32,8 @@ const otherSession = { sid: 'xyz321', sess: { cookie: { - originalMaxAge: 2880000, - expires: new Date(Date.now() + 86400000).toDateString(), + originalMaxAge: minutesToMilliseconds(48), + expires: addDays(Date.now(), 1).toDateString(), secure: false, httpOnly: true, path: '/', diff --git a/src/test/e2e/services/user-service.e2e.test.ts b/src/test/e2e/services/user-service.e2e.test.ts index 0934ef699b..2de89fe260 100644 --- a/src/test/e2e/services/user-service.e2e.test.ts +++ b/src/test/e2e/services/user-service.e2e.test.ts @@ -12,6 +12,7 @@ import { IRole } from '../../../lib/types/stores/access-store'; import { RoleName } from '../../../lib/types/model'; import SettingService from '../../../lib/services/setting-service'; import { simpleAuthKey } from '../../../lib/types/settings/simple-auth-settings'; +import { addDays, minutesToMilliseconds } from 'date-fns'; let db; let stores; @@ -161,8 +162,8 @@ test("deleting a user should delete the user's sessions", async () => { sid: 'xyz321', sess: { cookie: { - originalMaxAge: 2880000, - expires: new Date(Date.now() + 86400000).toDateString(), + originalMaxAge: minutesToMilliseconds(48), + expires: addDays(Date.now(), 1).toDateString(), secure: false, httpOnly: true, path: '/', diff --git a/src/test/e2e/stores/client-metrics-store-v2.e2e.test.ts b/src/test/e2e/stores/client-metrics-store-v2.e2e.test.ts index f9619547ef..af2b6d908a 100644 --- a/src/test/e2e/stores/client-metrics-store-v2.e2e.test.ts +++ b/src/test/e2e/stores/client-metrics-store-v2.e2e.test.ts @@ -1,4 +1,4 @@ -import { subDays } from 'date-fns'; +import { addHours, set, subDays } from 'date-fns'; import dbInit from '../helpers/database-init'; import getLogger from '../../fixtures/no-logger'; import { IUnleashStores } from '../../../lib/types'; @@ -66,10 +66,9 @@ test('Should "increment" metrics within same hour', async () => { }); test('Should get individual metrics outside same hour', async () => { - const d1 = new Date(); - const d2 = new Date(); - d1.setHours(10, 10, 11); - d2.setHours(11, 10, 11); + const d1 = set(Date.now(), { hours: 10, minutes: 10, seconds: 11 }); + const d2 = addHours(d1, 1); + const metrics: IClientMetricsEnv[] = [ { featureName: 'demo', @@ -265,7 +264,7 @@ test('Should not fail on undefined list of metrics', async () => { }); test('Should return delete old metric', async () => { - const twoDaysAgo = subDays(new Date(), 2); + const twoDaysAgo = subDays(Date.now(), 2); const metrics: IClientMetricsEnv[] = [ { @@ -311,7 +310,7 @@ test('Should return delete old metric', async () => { }); test('Should get metric', async () => { - const twoDaysAgo = subDays(new Date(), 2); + const twoDaysAgo = subDays(Date.now(), 2); const metrics: IClientMetricsEnv[] = [ { diff --git a/src/test/e2e/stores/event-store.e2e.test.ts b/src/test/e2e/stores/event-store.e2e.test.ts index 1c903c9e4f..186e6a1af7 100644 --- a/src/test/e2e/stores/event-store.e2e.test.ts +++ b/src/test/e2e/stores/event-store.e2e.test.ts @@ -102,7 +102,7 @@ test('Should be able to store multiple events at once', async () => { const seen = []; eventStore.on(APPLICATION_CREATED, (e) => seen.push(e)); await eventStore.batchStore([event1, event2, event3]); - await jest.advanceTimersByTime(100); + jest.advanceTimersByTime(100); expect(seen.length).toBe(3); seen.forEach((e) => { expect(e.id).toBeTruthy(); diff --git a/yarn.lock b/yarn.lock index 6279ef94a9..806bf1af5c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4996,11 +4996,6 @@ module-not-found-error@^1.0.1: resolved "https://registry.npmjs.org/module-not-found-error/-/module-not-found-error-1.0.1.tgz" integrity sha1-z4tP9PKWQGdNbN0CsOO8UjwrvcA= -moment@^2.24.0: - version "2.29.1" - resolved "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz" - integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== - "mongodb-uri@>= 0.9.7": version "0.9.7" resolved "https://registry.npmjs.org/mongodb-uri/-/mongodb-uri-0.9.7.tgz"