diff --git a/.eslintignore b/.eslintignore index e715d16eb9..ea57da6ed5 100644 --- a/.eslintignore +++ b/.eslintignore @@ -8,3 +8,4 @@ website/translated_docs website/core website/pages websitev2 +setupJest.js diff --git a/.eslintrc b/.eslintrc index cdffb22080..b08fb83138 100644 --- a/.eslintrc +++ b/.eslintrc @@ -5,18 +5,17 @@ }, "extends": [ "airbnb-typescript/base", - "prettier" + "plugin:prettier/recommended" ], "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaVersion": 2019, "project": "./tsconfig.json" }, - "plugins": ["prettier","@typescript-eslint"], + "plugins": ["@typescript-eslint","prettier"], "root": true, "rules": { "@typescript-eslint/no-var-requires": 0, - "@typescript-eslint/indent": ["error", 4], "@typescript-eslint/naming-convention": 0, "@typescript-eslint/space-before-function-paren": 0, "import/prefer-default-export": 0, @@ -46,10 +45,15 @@ "files": ["*.ts", "*.tsx"], "rules": { "@typescript-eslint/explicit-module-boundary-types": ["error"], - "@typescript-eslint/indent": ["error"], "@typescript-eslint/naming-convention": ["error"], "@typescript-eslint/space-before-function-paren": ["error"] } + }, + { + "files": ["src/test/e2e/helpers/test-helper.ts"], + "rules": { + "import/no-extraneous-dependencies": "off" + } } ], "ignorePatterns": ["**/docs/api/oas/", "examples/**"] diff --git a/package.json b/package.json index ec8ec6b93f..782622b956 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ "bugs": { "url": "https://github.com/unleash/unleash/issues" }, + "types": "./dist/lib/types/index.d.js", "engines": { "node": ">=14" }, @@ -43,6 +44,10 @@ "clean": "del-cli --force dist" }, "jest": { + "automock": false, + "setupFiles": [ + "./setupJest.js" + ], "transform": { "^.+\\.tsx?$": "ts-jest" }, @@ -109,37 +114,42 @@ "devDependencies": { "@types/bcrypt": "^5.0.0", "@types/express": "^4.17.11", + "@types/express-session": "^1.17.4", "@types/jest": "^26.0.23", + "@types/js-yaml": "^3.12.7", + "@types/memoizee": "^0.4.6", "@types/node": "^15.6.0", "@types/node-fetch": "^2.5.10", "@types/nodemailer": "^6.4.1", "@types/owasp-password-strength-test": "^1.3.0", "@types/stoppable": "^1.1.1", + "@types/supertest": "^2.0.11", "@typescript-eslint/eslint-plugin": "^4.22.0", "@typescript-eslint/parser": "^4.22.0", "copyfiles": "^2.4.1", "coveralls": "^3.1.0", "del-cli": "^4.0.1", - "eslint": "^6.8.0", + "eslint": "^7.32.0", "eslint-config-airbnb-base": "^14.2.1", "eslint-config-airbnb-typescript": "^12.3.1", - "eslint-config-prettier": "^8.1.0", - "eslint-plugin-import": "^2.22.1", - "eslint-plugin-prettier": "^3.3.1", + "eslint-config-prettier": "^8.3.0", + "eslint-plugin-import": "^2.23.4", + "eslint-plugin-prettier": "^3.4.0", "faker": "^5.5.3", "fetch-mock": "^9.11.0", "husky": "^4.2.3", - "jest": "^27.0.1", + "jest": "^27.0.6", + "jest-fetch-mock": "^3.0.3", "lint-staged": "^11.0.0", - "prettier": "^1.19.1", + "prettier": "^2.3.2", "proxyquire": "^2.1.3", "source-map-support": "^0.5.19", "superagent": "^6.1.0", "supertest": "^6.1.3", - "ts-jest": "^27.0.0", - "ts-node": "^10.0.0", + "ts-jest": "^27.0.4", + "ts-node": "^10.1.0", "tsc-watch": "^4.4.0", - "typescript": "^4.2.4" + "typescript": "^4.3.5" }, "resolutions": { "set-value": "^2.0.1", diff --git a/setupJest.js b/setupJest.js new file mode 100644 index 0000000000..0a2321c9ce --- /dev/null +++ b/setupJest.js @@ -0,0 +1,2 @@ +// adds the 'fetchMock' global variable and rewires 'fetch' global to call 'fetchMock' instead of the real implementation +require('jest-fetch-mock').enableMocks(); diff --git a/snapshots/dist/lib/addons/slack.test.js.md b/snapshots/dist/lib/addons/slack.test.js.md deleted file mode 100644 index 6cf55b999a..0000000000 --- a/snapshots/dist/lib/addons/slack.test.js.md +++ /dev/null @@ -1,11 +0,0 @@ -# Snapshot report for `dist/lib/addons/slack.test.js` - -The actual snapshot is saved in `slack.test.js.snap`. - -Generated by [AVA](https://avajs.dev). - -## Should call slack webhook - -> Snapshot 1 - - '{"username":"Unleash","icon_emoji":":unleash:","text":"some@user.com created feature toggle \\n*Enabled*: no | *Type*: undefined | *Project*: undefined\\n*Activation strategies*: ```- name: default\\n```","channel":"#undefined","attachments":[{"actions":[{"name":"featureToggle","text":"Open in Unleash","type":"button","value":"featureToggle","style":"primary","url":"http://some-url.com/#/features/strategies/some-toggle"}]}]}' diff --git a/snapshots/src/lib/addons/datadog.test.js.md b/snapshots/src/lib/addons/datadog.test.js.md deleted file mode 100644 index ccff468334..0000000000 --- a/snapshots/src/lib/addons/datadog.test.js.md +++ /dev/null @@ -1,17 +0,0 @@ -# Snapshot report for `src/lib/addons/datadog.test.js` - -The actual snapshot is saved in `datadog.test.js.snap`. - -Generated by [AVA](https://avajs.dev). - -## Should call datadog webhook - -> Snapshot 1 - - '{"text":"%%% \\n some@user.com created feature toggle [some-toggle](http://some-url.com/features/strategies/some-toggle)\\n**Enabled**: no | **Type**: undefined | **Project**: undefined\\n**Activation strategies**: ```- name: default\\n``` \\n %%% ","title":"Unleash notification update"}' - -## Should call datadog webhook for archived toggle - -> Snapshot 1 - - '{"text":"%%% \\n The feature toggle *[some-toggle](http://some-url.com/archive/strategies/some-toggle)* was *archived* by some@user.com. \\n %%% ","title":"Unleash notification update"}' diff --git a/snapshots/src/lib/addons/slack.test.js.md b/snapshots/src/lib/addons/slack.test.js.md deleted file mode 100644 index 75384818fa..0000000000 --- a/snapshots/src/lib/addons/slack.test.js.md +++ /dev/null @@ -1,17 +0,0 @@ -# Snapshot report for `src/lib/addons/slack.test.js` - -The actual snapshot is saved in `slack.test.js.snap`. - -Generated by [AVA](https://avajs.dev). - -## Should call slack webhook - -> Snapshot 1 - - '{"username":"Unleash","icon_emoji":":unleash:","text":"some@user.com created feature toggle \\n*Enabled*: no | *Type*: undefined | *Project*: undefined\\n*Activation strategies*: ```- name: default\\n```","channel":"#undefined","attachments":[{"actions":[{"name":"featureToggle","text":"Open in Unleash","type":"button","value":"featureToggle","style":"primary","url":"http://some-url.com/features/strategies/some-toggle"}]}]}' - -## Should call slack webhook for archived toggle - -> Snapshot 1 - - '{"username":"Unleash","icon_emoji":":unleash:","text":"The feature toggle ** was *archived* by some@user.com.","channel":"#undefined","attachments":[{"actions":[{"name":"featureToggle","text":"Open in Unleash","type":"button","value":"featureToggle","style":"primary","url":"http://some-url.com/archive/strategies/some-toggle"}]}]}' diff --git a/snapshots/src/lib/addons/teams.test.js.md b/snapshots/src/lib/addons/teams.test.js.md deleted file mode 100644 index 578272830e..0000000000 --- a/snapshots/src/lib/addons/teams.test.js.md +++ /dev/null @@ -1,17 +0,0 @@ -# Snapshot report for `src/lib/addons/teams.test.js` - -The actual snapshot is saved in `teams.test.js.snap`. - -Generated by [AVA](https://avajs.dev). - -## Should call teams webhook - -> Snapshot 1 - - '{"themeColor":"0076D7","summary":"Message","sections":[{"activityTitle":"Feature toggle some-toggle | *Type*: undefined | *Project*: undefined
*Activation strategies*: \\n- name: default\\n","activitySubtitle":"Unleash notification update","facts":[{"name":"User","value":"some@user.com"},{"name":"Action","value":"Create"},{"name":"Enabled","value":"*no*"}]}],"potentialAction":[{"@type":"OpenUri","name":"Go to feature","targets":[{"os":"default","uri":"http://some-url.com/features/strategies/some-toggle"}]}]}' - -## Should call teams webhook for archived toggle - -> Snapshot 1 - - '{"themeColor":"0076D7","summary":"Message","sections":[{"activityTitle":"The feature toggle *some-toggle* was *archived*","activitySubtitle":"Unleash notification update","facts":[{"name":"User","value":"some@user.com"},{"name":"Action","value":"feature-archived"},{"name":"Enabled","value":"*no*"}]}],"potentialAction":[{"@type":"OpenUri","name":"Go to feature","targets":[{"os":"default","uri":"http://some-url.com/archive/strategies/some-toggle"}]}]}' diff --git a/src/lib/addons/__snapshots__/datadog.test.js.snap b/src/lib/addons/__snapshots__/datadog.test.ts.snap similarity index 100% rename from src/lib/addons/__snapshots__/datadog.test.js.snap rename to src/lib/addons/__snapshots__/datadog.test.ts.snap diff --git a/src/lib/addons/__snapshots__/slack.test.js.snap b/src/lib/addons/__snapshots__/slack.test.ts.snap similarity index 100% rename from src/lib/addons/__snapshots__/slack.test.js.snap rename to src/lib/addons/__snapshots__/slack.test.ts.snap diff --git a/src/lib/addons/__snapshots__/teams.test.js.snap b/src/lib/addons/__snapshots__/teams.test.ts.snap similarity index 100% rename from src/lib/addons/__snapshots__/teams.test.js.snap rename to src/lib/addons/__snapshots__/teams.test.ts.snap diff --git a/src/lib/addons/addon-schema.js b/src/lib/addons/addon-schema.ts similarity index 60% rename from src/lib/addons/addon-schema.js rename to src/lib/addons/addon-schema.ts index ea92147d7c..78ea935c3d 100644 --- a/src/lib/addons/addon-schema.js +++ b/src/lib/addons/addon-schema.ts @@ -1,8 +1,8 @@ -const joi = require('joi'); -const { nameType } = require('../routes/admin-api/util'); -const { tagTypeSchema } = require('../services/tag-type-schema'); +import joi from 'joi'; +import { nameType } from '../routes/admin-api/util'; +import { tagTypeSchema } from '../services/tag-type-schema'; -const addonDefinitionSchema = joi.object().keys({ +export const addonDefinitionSchema = joi.object().keys({ name: nameType, displayName: joi.string(), documentationUrl: joi.string().uri({ scheme: [/https?/] }), @@ -21,16 +21,6 @@ const addonDefinitionSchema = joi.object().keys({ sensitive: joi.boolean().default(false), }), ), - events: joi - .array() - .optional() - .items(joi.string()), - tagTypes: joi - .array() - .optional() - .items(tagTypeSchema), + events: joi.array().optional().items(joi.string()), + tagTypes: joi.array().optional().items(tagTypeSchema), }); - -module.exports = { - addonDefinitionSchema, -}; diff --git a/src/lib/addons/addon.test.js b/src/lib/addons/addon.test.js deleted file mode 100644 index 79ccee5667..0000000000 --- a/src/lib/addons/addon.test.js +++ /dev/null @@ -1,71 +0,0 @@ -const fetchMock = require('fetch-mock').sandbox(); -const noLogger = require('../../test/fixtures/no-logger'); - -jest.mock('node-fetch', () => fetchMock); - -const Addon = require('./addon'); - -beforeEach(() => { - fetchMock.restore(); - fetchMock.reset(); -}); - -const definition = { - name: 'test', - displayName: 'test', - description: 'hello', -}; - -test('Retries if fetch throws', async () => { - const url = 'https://test.some.com'; - jest.useFakeTimers('modern'); - const addon = new Addon(definition, { - getLogger: noLogger, - }); - fetchMock - .once( - { - name: 'rejection', - type: 'POST', - url: `begin:${url}`, - }, - () => { - throw new Error('Network error'); - }, - ) - .mock( - { - name: 'acceptance', - type: 'POST', - url: `begin:${url}`, - }, - 201, - ); - await addon.fetchRetry(url); - jest.advanceTimersByTime(1000); - expect(fetchMock.done()).toBe(true); - jest.useRealTimers(); -}); - -test('does not crash even if fetch throws', async () => { - const url = 'https://test.some.com'; - jest.useFakeTimers('modern'); - const addon = new Addon(definition, { - getLogger: noLogger, - }); - fetchMock.mock( - { - name: 'rejection', - type: 'POST', - url: `begin:${url}`, - }, - () => { - throw new Error('Network error'); - }, - ); - await addon.fetchRetry(url); - jest.advanceTimersByTime(1000); - expect(fetchMock.done()).toBe(true); - expect(fetchMock.calls()).toHaveLength(2); - jest.useRealTimers(); -}); diff --git a/src/lib/addons/addon.test.ts b/src/lib/addons/addon.test.ts new file mode 100644 index 0000000000..0282166608 --- /dev/null +++ b/src/lib/addons/addon.test.ts @@ -0,0 +1,40 @@ +import fetchMock from 'jest-fetch-mock'; +import noLogger from '../../test/fixtures/no-logger'; + +import SlackAddon from './slack'; + +beforeEach(() => { + fetchMock.resetMocks(); +}); + +test('Retries if fetch throws', async () => { + const url = 'https://test.some.com'; + jest.useFakeTimers('modern'); + const addon = new SlackAddon({ + getLogger: noLogger, + unleashUrl: url, + }); + fetchMock.mockIf('https://test.some.com', 'OK', { + status: 201, + statusText: 'ACCEPTED', + }); + await addon.fetchRetry(url); + jest.advanceTimersByTime(1000); + jest.useRealTimers(); +}); + +test('does not crash even if fetch throws', async () => { + const url = 'https://test.some.com'; + jest.useFakeTimers('modern'); + const addon = new SlackAddon({ + getLogger: noLogger, + unleashUrl: url, + }); + fetchMock.mockResponse(() => { + throw new Error('Network error'); + }); + await addon.fetchRetry(url); + jest.advanceTimersByTime(1000); + expect(fetchMock.mock.calls).toHaveLength(2); + jest.useRealTimers(); +}); diff --git a/src/lib/addons/addon.js b/src/lib/addons/addon.ts similarity index 54% rename from src/lib/addons/addon.js rename to src/lib/addons/addon.ts index a1c0fd61f9..124af0fbb2 100644 --- a/src/lib/addons/addon.js +++ b/src/lib/addons/addon.ts @@ -1,10 +1,20 @@ -'use strict'; +import fetch, { Response } from 'node-fetch'; +import { addonDefinitionSchema } from './addon-schema'; +import { IUnleashConfig } from '../types/option'; +import { Logger } from '../logger'; +import { IAddonDefinition, IEvent } from '../types/model'; -const fetch = require('node-fetch'); -const { addonDefinitionSchema } = require('./addon-schema'); +export default abstract class Addon { + logger: Logger; -class Addon { - constructor(definition, { getLogger }) { + _name: string; + + _definition: IAddonDefinition; + + constructor( + definition: IAddonDefinition, + { getLogger }: Pick, + ) { this.logger = getLogger(`addon/${definition.name}`); const { error } = addonDefinitionSchema.validate(definition); if (error) { @@ -18,15 +28,20 @@ class Addon { this._definition = definition; } - get name() { + get name(): string { return this._name; } - get definition() { + get definition(): IAddonDefinition { return this._definition; } - async fetchRetry(url, options = {}, retries = 1, backoff = 300) { + async fetchRetry( + url: string, + options = {}, + retries: number = 1, + backoff: number = 300, + ): Promise { const retryCodes = [408, 500, 502, 503, 504, 522, 524]; let res; try { @@ -45,6 +60,7 @@ class Addon { } return res; } -} -module.exports = Addon; + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + abstract handleEvent(event: IEvent, parameters: any): Promise; +} diff --git a/src/lib/addons/datadog-definition.js b/src/lib/addons/datadog-definition.ts similarity index 87% rename from src/lib/addons/datadog-definition.js rename to src/lib/addons/datadog-definition.ts index 9709fe38e8..1f27b38d6d 100644 --- a/src/lib/addons/datadog-definition.js +++ b/src/lib/addons/datadog-definition.ts @@ -1,15 +1,14 @@ -'use strict'; - -const { +import { FEATURE_CREATED, FEATURE_UPDATED, FEATURE_ARCHIVED, FEATURE_REVIVED, FEATURE_STALE_ON, FEATURE_STALE_OFF, -} = require('../types/events'); +} from '../types/events'; +import { IAddonDefinition } from '../types/model'; -module.exports = { +const dataDogDefinition: IAddonDefinition = { name: 'datadog', displayName: 'Datadog', description: 'Allows Unleash to post updates to Datadog.', @@ -22,6 +21,7 @@ module.exports = { 'Default url: https://api.datadoghq.com/api/v1/events. Needs to be changed if your not using the US1 site.', type: 'url', required: false, + sensitive: false, }, { name: 'apiKey', @@ -50,3 +50,5 @@ module.exports = { }, ], }; + +export default dataDogDefinition; diff --git a/src/lib/addons/datadog.test.js b/src/lib/addons/datadog.test.ts similarity index 57% rename from src/lib/addons/datadog.test.js rename to src/lib/addons/datadog.test.ts index f77497ff12..f77b4c05d5 100644 --- a/src/lib/addons/datadog.test.js +++ b/src/lib/addons/datadog.test.ts @@ -1,31 +1,44 @@ -const { FEATURE_CREATED, FEATURE_ARCHIVED } = require('../types/events'); +import { FEATURE_CREATED, FEATURE_ARCHIVED } from '../types/events'; +import { Logger } from '../logger'; + +import DatadogAddon from './datadog'; + +import noLogger from '../../test/fixtures/no-logger'; +import { IEvent } from '../types/model'; + +let fetchRetryCalls: any[] = []; jest.mock( './addon', () => class Addon { + logger: Logger; + constructor(definition, { getLogger }) { this.logger = getLogger('addon/test'); - this.fetchRetryCalls = []; + fetchRetryCalls = []; } async fetchRetry(url, options, retries, backoff) { - this.fetchRetryCalls.push({ url, options, retries, backoff }); + fetchRetryCalls.push({ + url, + options, + retries, + backoff, + }); return Promise.resolve({ status: 200 }); } }, ); -const DatadogAddon = require('./datadog'); - -const noLogger = require('../../test/fixtures/no-logger'); - test('Should call datadog webhook', async () => { const addon = new DatadogAddon({ getLogger: noLogger, unleashUrl: 'http://some-url.com', }); - const event = { + const event: IEvent = { + id: 1, + createdAt: new Date(), type: FEATURE_CREATED, createdBy: 'some@user.com', data: { @@ -40,9 +53,9 @@ test('Should call datadog webhook', async () => { }; await addon.handleEvent(event, parameters); - expect(addon.fetchRetryCalls.length).toBe(1); - expect(addon.fetchRetryCalls[0].url).toBe(parameters.url); - expect(addon.fetchRetryCalls[0].options.body).toMatchSnapshot(); + expect(fetchRetryCalls.length).toBe(1); + expect(fetchRetryCalls[0].url).toBe(parameters.url); + expect(fetchRetryCalls[0].options.body).toMatchSnapshot(); }); test('Should call datadog webhook for archived toggle', async () => { @@ -50,7 +63,9 @@ test('Should call datadog webhook for archived toggle', async () => { getLogger: noLogger, unleashUrl: 'http://some-url.com', }); - const event = { + const event: IEvent = { + id: 2, + createdAt: new Date(), type: FEATURE_ARCHIVED, createdBy: 'some@user.com', data: { @@ -63,7 +78,7 @@ test('Should call datadog webhook for archived toggle', async () => { }; await addon.handleEvent(event, parameters); - expect(addon.fetchRetryCalls.length).toBe(1); - expect(addon.fetchRetryCalls[0].url).toBe(parameters.url); - expect(addon.fetchRetryCalls[0].options.body).toMatchSnapshot(); + expect(fetchRetryCalls.length).toBe(1); + expect(fetchRetryCalls[0].url).toBe(parameters.url); + expect(fetchRetryCalls[0].options.body).toMatchSnapshot(); }); diff --git a/src/lib/addons/datadog.js b/src/lib/addons/datadog.ts similarity index 74% rename from src/lib/addons/datadog.js rename to src/lib/addons/datadog.ts index 77f1aeabd8..e72bdb660c 100644 --- a/src/lib/addons/datadog.js +++ b/src/lib/addons/datadog.ts @@ -1,30 +1,30 @@ -'use strict'; - -const YAML = require('js-yaml'); -const Addon = require('./addon'); - -const { +import YAML from 'js-yaml'; +import Addon from './addon'; +import { FEATURE_CREATED, FEATURE_UPDATED, FEATURE_ARCHIVED, FEATURE_REVIVED, FEATURE_STALE_ON, FEATURE_STALE_OFF, -} = require('../types/events'); +} from '../types/events'; -const definition = require('./datadog-definition'); +import definition from './datadog-definition'; +import { LogProvider } from '../logger'; +import { IEvent } from '../types/model'; -class DatadogAddon extends Addon { - constructor(args) { - super(definition, args); - this.unleashUrl = args.unleashUrl; +export default class DatadogAddon extends Addon { + unleashUrl: string; + + constructor(config: { unleashUrl: string; getLogger: LogProvider }) { + super(definition, config); + this.unleashUrl = config.unleashUrl; } - async handleEvent(event, parameters) { - const { - url = 'https://api.datadoghq.com/api/v1/events', - apiKey, - } = parameters; + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + async handleEvent(event: IEvent, parameters: any): Promise { + const { url = 'https://api.datadoghq.com/api/v1/events', apiKey } = + parameters; let text; if ([FEATURE_ARCHIVED, FEATURE_REVIVED].includes(event.type)) { @@ -37,7 +37,7 @@ class DatadogAddon extends Addon { const { tags: eventTags } = event; const tags = - eventTags && eventTags.map(tag => `${tag.value}:${tag.type}`); + eventTags && eventTags.map((tag) => `${tag.value}:${tag.type}`); const body = { text: `%%% \n ${text} \n %%% `, title: 'Unleash notification update', @@ -58,12 +58,12 @@ class DatadogAddon extends Addon { ); } - featureLink(event) { + featureLink(event: IEvent): string { const path = event.type === FEATURE_ARCHIVED ? 'archive' : 'features'; return `${this.unleashUrl}/${path}/strategies/${event.data.name}`; } - generateStaleText(event) { + generateStaleText(event: IEvent): string { const { createdBy, data, type } = event; const isStale = type === FEATURE_STALE_ON; const feature = `[${data.name}](${this.featureLink(event)})`; @@ -75,14 +75,14 @@ This was changed by ${createdBy}.`; return `The feature toggle *${feature}* was *unmarked as stale* by ${createdBy}.`; } - generateArchivedText(event) { + generateArchivedText(event: IEvent): string { const { createdBy, data, type } = event; const action = type === FEATURE_ARCHIVED ? 'archived' : 'revived'; const feature = `[${data.name}](${this.featureLink(event)})`; return `The feature toggle *${feature}* was *${action}* by ${createdBy}.`; } - generateText(event) { + generateText(event: IEvent): string { const { createdBy, data, type } = event; const action = this.getAction(type); const feature = `[${data.name}](${this.featureLink(event)})`; @@ -90,7 +90,7 @@ This was changed by ${createdBy}.`; const stale = data.stale ? '("stale")' : ''; const typeStr = `**Type**: ${data.type}`; const project = `**Project**: ${data.project}`; - const strategies = `**Activation strategies**: \`\`\`${YAML.safeDump( + const strategies = `**Activation strategies**: \`\`\`${YAML.dump( data.strategies, { skipInvalid: true }, )}\`\`\``; @@ -99,7 +99,7 @@ ${enabled}${stale} | ${typeStr} | ${project} ${strategies}`; } - getAction(type) { + getAction(type: string): string { switch (type) { case FEATURE_CREATED: return 'created'; @@ -110,5 +110,3 @@ ${strategies}`; } } } - -module.exports = DatadogAddon; diff --git a/src/lib/addons/index.js b/src/lib/addons/index.js deleted file mode 100644 index 4c839704ee..0000000000 --- a/src/lib/addons/index.js +++ /dev/null @@ -1,8 +0,0 @@ -const webhook = require('./webhook'); -const slackAddon = require('./slack'); -const teamsAddon = require('./teams'); -const datadogAddon = require('./datadog'); - -const addons = [webhook, slackAddon, teamsAddon, datadogAddon]; - -module.exports = addons; diff --git a/src/lib/addons/index.ts b/src/lib/addons/index.ts new file mode 100644 index 0000000000..cc75756c9d --- /dev/null +++ b/src/lib/addons/index.ts @@ -0,0 +1,27 @@ +import Webhook from './webhook'; +import SlackAddon from './slack'; +import TeamsAddon from './teams'; +import DatadogAddon from './datadog'; +import Addon from './addon'; +import { LogProvider } from '../logger'; + +export interface IAddonProviders { + [key: string]: Addon; +} + +export const getAddons: (args: { + getLogger: LogProvider; + unleashUrl: string; +}) => IAddonProviders = ({ getLogger, unleashUrl }) => { + const addons = [ + new Webhook({ getLogger }), + new SlackAddon({ getLogger, unleashUrl }), + new TeamsAddon({ getLogger, unleashUrl }), + new DatadogAddon({ getLogger, unleashUrl }), + ]; + return addons.reduce((map, addon) => { + // eslint-disable-next-line no-param-reassign + map[addon.name] = addon; + return map; + }, {}); +}; diff --git a/src/lib/addons/jira-comment-definition.js b/src/lib/addons/jira-comment-definition.js deleted file mode 100644 index f937260a83..0000000000 --- a/src/lib/addons/jira-comment-definition.js +++ /dev/null @@ -1,52 +0,0 @@ -const { - FEATURE_CREATED, - FEATURE_UPDATED, - FEATURE_ARCHIVED, - FEATURE_REVIVED, -} = require('../types/events'); - -module.exports = { - name: 'jira-comment', - displayName: 'Jira Commenter', - description: 'Allows Unleash to post comments to JIRA issues', - parameters: [ - { - name: 'baseUrl', - displayName: 'Jira base url e.g. https://myjira.atlassian.net', - type: 'url', - required: true, - }, - { - name: 'apiKey', - displayName: 'Jira API token', - description: - 'Used to authenticate against JIRA REST api, needs to be for a user with comment access to issues. ' + - 'Add a new key at https://id.atlassian.com/manage-profile/security/api-tokens when logged in as the user you want Unleash to use', - type: 'text', - required: true, - sensitive: true, - }, - { - name: 'user', - displayName: 'JIRA username', - description: - 'Used together with API key to authenticate against JIRA. Since Unleash adds comments as this user, it is a good idea to create a separate user', - type: 'text', - required: true, - }, - ], - events: [ - FEATURE_CREATED, - FEATURE_UPDATED, - FEATURE_ARCHIVED, - FEATURE_REVIVED, - ], - tagTypes: [ - { - name: 'jira', - description: - 'Jira tag used by the jira addon to specify the JIRA issue to comment on', - icon: 'J', - }, - ], -}; diff --git a/src/lib/addons/jira-comment.js b/src/lib/addons/jira-comment.js deleted file mode 100644 index 4b9f9818bf..0000000000 --- a/src/lib/addons/jira-comment.js +++ /dev/null @@ -1,116 +0,0 @@ -'use strict'; - -const Addon = require('./addon'); -const definition = require('./jira-comment-definition'); -const { - FEATURE_CREATED, - FEATURE_UPDATED, - FEATURE_REVIVED, - FEATURE_ARCHIVED, -} = require('../types/events'); - -class JiraAddon extends Addon { - constructor(args) { - super(definition, args); - this.unleashUrl = args.unleashUrl; - } - - async handleEvent(event, parameters) { - const { type: eventName } = event; - const { baseUrl, user, apiKey } = parameters; - const issuesToPostTo = this.findJiraTag(event); - const action = this.getAction(eventName); - - const body = this.formatBody(event, action); - - const requests = issuesToPostTo.map(async issueTag => { - const issue = issueTag.value; - const issueUrl = `${baseUrl}/rest/api/3/issue/${issue}/comment`; - - const requestOpts = { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Accept: 'application/json', - Authorization: this.buildAuthHeader(user, apiKey), - }, - body, - }; - - return this.fetchRetry(issueUrl, requestOpts); - }); - - const results = await Promise.all(requests); - const codes = results.map(res => res.status).join(', '); - this.logger.info(`Handled event ${event.type}. Status codes=${codes}`); - } - - getAction(eventName) { - switch (eventName) { - case FEATURE_CREATED: - return 'created'; - case FEATURE_UPDATED: - return 'updated'; - case FEATURE_ARCHIVED: - return 'archived'; - case FEATURE_REVIVED: - return 'revived'; - default: - return 'unknown'; - } - } - - encode(str) { - return Buffer.from(str, 'utf-8').toString('base64'); - } - - formatBody(event, action) { - const featureName = event.data.name; - const { createdBy } = event; - return JSON.stringify({ - body: { - type: 'doc', - version: 1, - content: [ - { - type: 'paragraph', - content: [ - { - type: 'text', - text: `Feature toggle "${featureName}" was ${action} by ${createdBy}`, - }, - ], - }, - { - type: 'paragraph', - content: [ - { - type: 'text', - text: 'To see what happened visit Unleash', - marks: [ - { - type: 'link', - attrs: { - href: `${this.unleashUrl}/features/strategies/${featureName}`, - title: 'Visit Unleash Admin UI', - }, - }, - ], - }, - ], - }, - ], - }, - }); - } - - buildAuthHeader(userName, apiKey) { - const base64 = this.encode(`${userName}:${apiKey}`); - return `Basic ${base64}`; - } - - findJiraTag({ tags }) { - return tags.filter(tag => tag.type === 'jira'); - } -} -module.exports = JiraAddon; diff --git a/src/lib/addons/jira-comment.test.js b/src/lib/addons/jira-comment.test.js deleted file mode 100644 index 2023e6babf..0000000000 --- a/src/lib/addons/jira-comment.test.js +++ /dev/null @@ -1,225 +0,0 @@ -const fetchMock = require('fetch-mock').sandbox(); -const noLogger = require('../../test/fixtures/no-logger'); - -jest.mock('node-fetch', () => fetchMock); - -const addonMocked = require('./addon'); - -jest.mock('./addon', () => addonMocked); -const JiraAddon = require('./jira-comment'); -const { addonDefinitionSchema } = require('./addon-schema'); - -beforeEach(() => { - fetchMock.restore(); - fetchMock.reset(); -}); - -test('Addon definition should validate', () => { - const { error } = addonDefinitionSchema.validate(JiraAddon.definition); - expect(error).toBe(undefined); -}); - -test('An update event should post updated comment with updater and link back to issue', async () => { - const jiraIssue = 'TEST-1'; - const jiraBaseUrl = 'https://test.jira.com'; - const addon = new JiraAddon({ - getLogger: noLogger, - unleashUrl: 'https://test.unleash.com', - }); - fetchMock.mock( - { url: `${jiraBaseUrl}/rest/api/3/issue/${jiraIssue}/comment` }, - 201, - ); - await addon.handleEvent( - { - createdBy: 'test@test.com', - type: 'feature-updated', - data: { - name: 'feature.toggle', - }, - tags: [{ type: 'jira', value: jiraIssue }], - }, - { - baseUrl: jiraBaseUrl, - user: 'test@test.com', - apiKey: 'test', - }, - ); - expect(fetchMock.calls(true).length).toBe(1); - expect(fetchMock.done()).toBe(true); -}); - -test('An event that is tagged with two tags causes two updates', async () => { - const jiraBaseUrl = 'https://test.jira.com'; - const addon = new JiraAddon({ - getLogger: noLogger, - unleashUrl: 'https://test.unleash.com', - }); - fetchMock.mock( - { - name: 'test-1', - url: `${jiraBaseUrl}/rest/api/3/issue/TEST-1/comment`, - }, - { - status: 201, - statusText: 'Accepted', - }, - ); - fetchMock.mock( - { - name: 'test-2', - url: `${jiraBaseUrl}/rest/api/3/issue/TEST-2/comment`, - }, - { - status: 201, - statusText: 'Accepted', - }, - ); - await addon.handleEvent( - { - createdBy: 'test@test.com', - type: 'feature-updated', - data: { - name: 'feature.toggle', - }, - tags: [ - { type: 'jira', value: 'TEST-1' }, - { type: 'jira', value: 'TEST-2' }, - ], - }, - { - baseUrl: 'https://test.jira.com', - user: 'test@test.com', - apiKey: 'test', - }, - ); - expect(fetchMock.done()).toBe(true); -}); - -test('An event with no jira tags will be ignored', async () => { - const addon = new JiraAddon({ - getLogger: noLogger, - unleashUrl: 'https://test.unleash.com', - }); - fetchMock.any(200); - await addon.handleEvent( - { - createdBy: 'test@test.com', - type: 'feature-updated', - data: { - name: 'feature.toggle', - }, - tags: [], - }, - { - baseUrl: 'https://test.jira.com', - user: 'test@test.com', - apiKey: 'test', - }, - ); - expect(fetchMock.calls().length).toBe(0); // No calls -}); - -test('Retries if error code in the 500s', async () => { - const jiraBaseUrl = 'https://test.jira.com'; - const jiraIssue = 'TEST-1'; - jest.useFakeTimers('modern'); - const addon = new JiraAddon({ - getLogger: noLogger, - unleashUrl: 'https://test.unleash.com', - }); - fetchMock - .once( - { - name: 'rejection', - type: 'POST', - url: 'begin:https://test.jira.com', - }, - 500, - ) - .mock( - { - name: 'acceptance', - type: 'POST', - url: 'begin:https://test.jira.com', - }, - 201, - ); - await addon.handleEvent( - { - type: 'feature-updated', - createdBy: 'test@test.com', - data: { - name: 'feature.toggle', - }, - tags: [{ type: 'jira', value: jiraIssue }], - }, - { - baseUrl: jiraBaseUrl, - user: 'test@test.com', - apiKey: 'test', - }, - ); - jest.advanceTimersByTime(1000); - expect(fetchMock.done()).toBe(true); -}); -test('Only retries once', async () => { - const jiraBaseUrl = 'https://test.jira.com'; - const jiraIssue = 'TEST-1'; - jest.useFakeTimers('modern'); - const addon = new JiraAddon({ - getLogger: noLogger, - unleashUrl: 'https://test.unleash.com', - }); - fetchMock.mock('*', 500, { repeat: 2 }); - await addon.handleEvent( - { - type: 'feature-updated', - createdBy: 'test@test.com', - data: { - name: 'feature.toggle', - }, - tags: [{ type: 'jira', value: jiraIssue }], - }, - { - baseUrl: jiraBaseUrl, - user: 'test@test.com', - apiKey: 'test', - }, - ); - jest.advanceTimersByTime(1000); - expect(fetchMock.done()).toBe(true); -}); - -test('Does not retry if a 4xx error is given', async () => { - const jiraBaseUrl = 'https://test.jira.com'; - const jiraIssue = 'TEST-1'; - const addon = new JiraAddon({ - getLogger: noLogger, - unleashUrl: 'https://test.unleash.com', - }); - fetchMock.once( - { - name: 'rejection', - type: 'POST', - url: 'begin:https://test.jira.com', - }, - 400, - ); - await addon.handleEvent( - { - type: 'feature-updated', - createdBy: 'test@test.com', - data: { - name: 'feature.toggle', - }, - tags: [{ type: 'jira', value: jiraIssue }], - }, - { - baseUrl: jiraBaseUrl, - user: 'test@test.com', - apiKey: 'test', - }, - ); - expect(fetchMock.done()).toBe(true); -}); diff --git a/src/lib/addons/slack-definition.js b/src/lib/addons/slack-definition.ts similarity index 87% rename from src/lib/addons/slack-definition.js rename to src/lib/addons/slack-definition.ts index 31d4124385..39a03d43ec 100644 --- a/src/lib/addons/slack-definition.js +++ b/src/lib/addons/slack-definition.ts @@ -1,15 +1,14 @@ -'use strict'; - -const { +import { FEATURE_CREATED, FEATURE_UPDATED, FEATURE_ARCHIVED, FEATURE_REVIVED, FEATURE_STALE_ON, FEATURE_STALE_OFF, -} = require('../types/events'); +} from '../types/events'; +import { IAddonDefinition } from '../types/model'; -module.exports = { +const slackDefinition: IAddonDefinition = { name: 'slack', displayName: 'Slack', description: 'Allows Unleash to post updates to Slack.', @@ -30,6 +29,7 @@ module.exports = { 'The username to use when posting messages to slack. Defaults to "Unleash".', type: 'text', required: false, + sensitive: false, }, { name: 'emojiIcon', @@ -39,6 +39,7 @@ module.exports = { 'The emoji_icon to use when posting messages to slack. Defaults to ":unleash:".', type: 'text', required: false, + sensitive: false, }, { name: 'defaultChannel', @@ -47,6 +48,7 @@ module.exports = { 'Default channel to post updates to if not specified in the slack-tag', type: 'text', required: true, + sensitive: false, }, ], events: [ @@ -66,3 +68,5 @@ module.exports = { }, ], }; + +export default slackDefinition; diff --git a/src/lib/addons/slack.test.js b/src/lib/addons/slack.test.ts similarity index 70% rename from src/lib/addons/slack.test.js rename to src/lib/addons/slack.test.ts index 9f1b4fbde2..2f57274d05 100644 --- a/src/lib/addons/slack.test.js +++ b/src/lib/addons/slack.test.ts @@ -1,31 +1,44 @@ -const { FEATURE_CREATED, FEATURE_ARCHIVED } = require('../types/events'); +import { FEATURE_CREATED, FEATURE_ARCHIVED } from '../types/events'; +import { Logger } from '../logger'; + +import SlackAddon from './slack'; + +import noLogger from '../../test/fixtures/no-logger'; +import { IEvent } from '../types/model'; + +let fetchRetryCalls: any[] = []; jest.mock( './addon', () => class Addon { + logger: Logger; + constructor(definition, { getLogger }) { this.logger = getLogger('addon/test'); - this.fetchRetryCalls = []; + fetchRetryCalls = []; } async fetchRetry(url, options, retries, backoff) { - this.fetchRetryCalls.push({ url, options, retries, backoff }); + fetchRetryCalls.push({ + url, + options, + retries, + backoff, + }); return Promise.resolve({ status: 200 }); } }, ); -const SlackAddon = require('./slack'); - -const noLogger = require('../../test/fixtures/no-logger'); - test('Should call slack webhook', async () => { const addon = new SlackAddon({ getLogger: noLogger, unleashUrl: 'http://some-url.com', }); - const event = { + const event: IEvent = { + id: 1, + createdAt: new Date(), type: FEATURE_CREATED, createdBy: 'some@user.com', data: { @@ -40,9 +53,9 @@ test('Should call slack webhook', async () => { }; await addon.handleEvent(event, parameters); - expect(addon.fetchRetryCalls.length).toBe(1); - expect(addon.fetchRetryCalls[0].url).toBe(parameters.url); - expect(addon.fetchRetryCalls[0].options.body).toMatchSnapshot(); + expect(fetchRetryCalls.length).toBe(1); + expect(fetchRetryCalls[0].url).toBe(parameters.url); + expect(fetchRetryCalls[0].options.body).toMatchSnapshot(); }); test('Should call slack webhook for archived toggle', async () => { @@ -50,7 +63,9 @@ test('Should call slack webhook for archived toggle', async () => { getLogger: noLogger, unleashUrl: 'http://some-url.com', }); - const event = { + const event: IEvent = { + id: 2, + createdAt: new Date(), type: FEATURE_ARCHIVED, createdBy: 'some@user.com', data: { @@ -63,9 +78,9 @@ test('Should call slack webhook for archived toggle', async () => { }; await addon.handleEvent(event, parameters); - expect(addon.fetchRetryCalls.length).toBe(1); - expect(addon.fetchRetryCalls[0].url).toBe(parameters.url); - expect(addon.fetchRetryCalls[0].options.body).toMatchSnapshot(); + expect(fetchRetryCalls.length).toBe(1); + expect(fetchRetryCalls[0].url).toBe(parameters.url); + expect(fetchRetryCalls[0].options.body).toMatchSnapshot(); }); test('Should use default channel', async () => { @@ -73,7 +88,9 @@ test('Should use default channel', async () => { getLogger: noLogger, unleashUrl: 'http://some-url.com', }); - const event = { + const event: IEvent = { + id: 3, + createdAt: new Date(), type: FEATURE_CREATED, createdBy: 'some@user.com', data: { @@ -90,7 +107,7 @@ test('Should use default channel', async () => { await addon.handleEvent(event, parameters); - const req = JSON.parse(addon.fetchRetryCalls[0].options.body); + const req = JSON.parse(fetchRetryCalls[0].options.body); expect(req.channel).toBe('#some-channel'); }); @@ -100,7 +117,9 @@ test('Should override default channel with data from tag', async () => { getLogger: noLogger, unleashUrl: 'http://some-url.com', }); - const event = { + const event: IEvent = { + id: 4, + createdAt: new Date(), type: FEATURE_CREATED, createdBy: 'some@user.com', data: { @@ -123,7 +142,7 @@ test('Should override default channel with data from tag', async () => { await addon.handleEvent(event, parameters); - const req = JSON.parse(addon.fetchRetryCalls[0].options.body); + const req = JSON.parse(fetchRetryCalls[0].options.body); expect(req.channel).toBe('#another-channel'); }); @@ -133,7 +152,9 @@ test('Should post to all channels in tags', async () => { getLogger: noLogger, unleashUrl: 'http://some-url.com', }); - const event = { + const event: IEvent = { + id: 5, + createdAt: new Date(), type: FEATURE_CREATED, createdBy: 'some@user.com', data: { @@ -160,10 +181,10 @@ test('Should post to all channels in tags', async () => { await addon.handleEvent(event, parameters); - const req1 = JSON.parse(addon.fetchRetryCalls[0].options.body); - const req2 = JSON.parse(addon.fetchRetryCalls[1].options.body); + const req1 = JSON.parse(fetchRetryCalls[0].options.body); + const req2 = JSON.parse(fetchRetryCalls[1].options.body); - expect(addon.fetchRetryCalls.length).toBe(2); + expect(fetchRetryCalls).toHaveLength(2); expect(req1.channel).toBe('#another-channel-1'); expect(req2.channel).toBe('#another-channel-2'); }); diff --git a/src/lib/addons/slack.js b/src/lib/addons/slack.ts similarity index 78% rename from src/lib/addons/slack.js rename to src/lib/addons/slack.ts index d2fc272895..a15c65b992 100644 --- a/src/lib/addons/slack.js +++ b/src/lib/addons/slack.ts @@ -1,7 +1,8 @@ -'use strict'; +import YAML from 'js-yaml'; +import Addon from './addon'; -const YAML = require('js-yaml'); -const Addon = require('./addon'); +import slackDefinition from './slack-definition'; +import { IAddonConfig, IEvent } from '../types/model'; const { FEATURE_CREATED, @@ -12,15 +13,16 @@ const { FEATURE_STALE_OFF, } = require('../types/events'); -const definition = require('./slack-definition'); +export default class SlackAddon extends Addon { + unleashUrl: string; -class SlackAddon extends Addon { - constructor(args) { - super(definition, args); + constructor(args: IAddonConfig) { + super(slackDefinition, args); this.unleashUrl = args.unleashUrl; } - async handleEvent(event, parameters) { + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + async handleEvent(event: IEvent, parameters: any): Promise { const { url, defaultChannel, @@ -44,7 +46,7 @@ class SlackAddon extends Addon { text = this.generateText(event); } - const requests = slackChannels.map(channel => { + const requests = slackChannels.map((channel) => { const body = { username, icon_emoji: iconEmoji, // eslint-disable-line camelcase @@ -76,20 +78,25 @@ class SlackAddon extends Addon { }); const results = await Promise.all(requests); - const codes = results.map(res => res.status).join(', '); + const codes = results.map((res) => res.status).join(', '); this.logger.info(`Handled event ${event.type}. Status codes=${codes}`); } - featureLink(event) { + featureLink(event: IEvent): string { const path = event.type === FEATURE_ARCHIVED ? 'archive' : 'features'; return `${this.unleashUrl}/${path}/strategies/${event.data.name}`; } - findSlackChannels({ tags = [] }) { - return tags.filter(tag => tag.type === 'slack').map(t => t.value); + findSlackChannels({ tags }: Pick): string[] { + if (tags) { + return tags + .filter((tag) => tag.type === 'slack') + .map((t) => t.value); + } + return []; } - generateStaleText(event) { + generateStaleText(event: IEvent): string { const { createdBy, data, type } = event; const isStale = type === FEATURE_STALE_ON; const feature = `<${this.featureLink(event)}|${data.name}>`; @@ -101,14 +108,14 @@ This was changed by ${createdBy}.`; return `The feature toggle *${feature}* was *unmarked as stale* by ${createdBy}.`; } - generateArchivedText(event) { + generateArchivedText(event: IEvent): string { const { createdBy, data, type } = event; const action = type === FEATURE_ARCHIVED ? 'archived' : 'revived'; const feature = `<${this.featureLink(event)}|${data.name}>`; return `The feature toggle *${feature}* was *${action}* by ${createdBy}.`; } - generateText(event) { + generateText(event: IEvent): string { const { createdBy, data, type } = event; const action = this.getAction(type); const feature = `<${this.featureLink(event)}|${data.name}>`; @@ -125,7 +132,7 @@ ${enabled}${stale} | ${typeStr} | ${project} ${strategies}`; } - getAction(type) { + getAction(type: string): string { switch (type) { case FEATURE_CREATED: return 'created'; diff --git a/src/lib/addons/teams-definition.js b/src/lib/addons/teams-definition.ts similarity index 81% rename from src/lib/addons/teams-definition.js rename to src/lib/addons/teams-definition.ts index 7177f443c6..b46239438b 100644 --- a/src/lib/addons/teams-definition.js +++ b/src/lib/addons/teams-definition.ts @@ -1,15 +1,14 @@ -'use strict'; - -const { +import { FEATURE_CREATED, FEATURE_UPDATED, FEATURE_ARCHIVED, FEATURE_REVIVED, FEATURE_STALE_ON, FEATURE_STALE_OFF, -} = require('../types/events'); +} from '../types/events'; +import { IAddonDefinition } from '../types/model'; -module.exports = { +const teamsDefinition: IAddonDefinition = { name: 'teams', displayName: 'Microsoft Teams', description: 'Allows Unleash to post updates to Microsoft Teams.', @@ -32,3 +31,5 @@ module.exports = { FEATURE_STALE_OFF, ], }; + +export default teamsDefinition; diff --git a/src/lib/addons/teams.test.js b/src/lib/addons/teams.test.ts similarity index 57% rename from src/lib/addons/teams.test.js rename to src/lib/addons/teams.test.ts index 8572843f44..8945454951 100644 --- a/src/lib/addons/teams.test.js +++ b/src/lib/addons/teams.test.ts @@ -1,31 +1,45 @@ -const { FEATURE_CREATED, FEATURE_ARCHIVED } = require('../types/events'); +import { Logger } from '../logger'; + +import { FEATURE_CREATED, FEATURE_ARCHIVED } from '../types/events'; + +import TeamsAddon from './teams'; + +import noLogger from '../../test/fixtures/no-logger'; +import { IEvent } from '../types/model'; + +let fetchRetryCalls: any[]; jest.mock( './addon', () => class Addon { + logger: Logger; + constructor(definition, { getLogger }) { this.logger = getLogger('addon/test'); - this.fetchRetryCalls = []; + fetchRetryCalls = []; } async fetchRetry(url, options, retries, backoff) { - this.fetchRetryCalls.push({ url, options, retries, backoff }); + fetchRetryCalls.push({ + url, + options, + retries, + backoff, + }); return Promise.resolve({ status: 200 }); } }, ); -const TeamsAddon = require('./teams'); - -const noLogger = require('../../test/fixtures/no-logger'); - test('Should call teams webhook', async () => { const addon = new TeamsAddon({ getLogger: noLogger, unleashUrl: 'http://some-url.com', }); - const event = { + const event: IEvent = { + id: 1, + createdAt: new Date(), type: FEATURE_CREATED, createdBy: 'some@user.com', data: { @@ -40,9 +54,9 @@ test('Should call teams webhook', async () => { }; await addon.handleEvent(event, parameters); - expect(addon.fetchRetryCalls.length).toBe(1); - expect(addon.fetchRetryCalls[0].url).toBe(parameters.url); - expect(addon.fetchRetryCalls[0].options.body).toMatchSnapshot(); + expect(fetchRetryCalls.length).toBe(1); + expect(fetchRetryCalls[0].url).toBe(parameters.url); + expect(fetchRetryCalls[0].options.body).toMatchSnapshot(); }); test('Should call teams webhook for archived toggle', async () => { @@ -51,6 +65,8 @@ test('Should call teams webhook for archived toggle', async () => { unleashUrl: 'http://some-url.com', }); const event = { + id: 1, + createdAt: new Date(), type: FEATURE_ARCHIVED, createdBy: 'some@user.com', data: { @@ -63,7 +79,7 @@ test('Should call teams webhook for archived toggle', async () => { }; await addon.handleEvent(event, parameters); - expect(addon.fetchRetryCalls.length).toBe(1); - expect(addon.fetchRetryCalls[0].url).toBe(parameters.url); - expect(addon.fetchRetryCalls[0].options.body).toMatchSnapshot(); + expect(fetchRetryCalls.length).toBe(1); + expect(fetchRetryCalls[0].url).toBe(parameters.url); + expect(fetchRetryCalls[0].options.body).toMatchSnapshot(); }); diff --git a/src/lib/addons/teams.js b/src/lib/addons/teams.ts similarity index 81% rename from src/lib/addons/teams.js rename to src/lib/addons/teams.ts index 7d24575510..1fe82e95c6 100644 --- a/src/lib/addons/teams.js +++ b/src/lib/addons/teams.ts @@ -1,26 +1,29 @@ -'use strict'; +import YAML from 'js-yaml'; +import Addon from './addon'; -const YAML = require('js-yaml'); -const Addon = require('./addon'); - -const { +import { FEATURE_CREATED, FEATURE_UPDATED, FEATURE_ARCHIVED, FEATURE_REVIVED, FEATURE_STALE_ON, FEATURE_STALE_OFF, -} = require('../types/events'); +} from '../types/events'; +import { LogProvider } from '../logger'; -const definition = require('./teams-definition'); +import teamsDefinition from './teams-definition'; +import { IEvent } from '../types/model'; -class TeamsAddon extends Addon { - constructor(args) { - super(definition, args); +export default class TeamsAddon extends Addon { + unleashUrl: string; + + constructor(args: { unleashUrl: string; getLogger: LogProvider }) { + super(teamsDefinition, args); this.unleashUrl = args.unleashUrl; } - async handleEvent(event, parameters) { + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + async handleEvent(event: IEvent, parameters: any): Promise { const { url } = parameters; const { createdBy, data, type } = event; let text = ''; @@ -82,12 +85,12 @@ class TeamsAddon extends Addon { ); } - featureLink(event) { + featureLink(event: IEvent): string { const path = event.type === FEATURE_ARCHIVED ? 'archive' : 'features'; return `${this.unleashUrl}/${path}/strategies/${event.data.name}`; } - generateStaleText(event) { + generateStaleText(event: IEvent): string { const { data, type } = event; const isStale = type === FEATURE_STALE_ON; if (isStale) { @@ -96,13 +99,13 @@ class TeamsAddon extends Addon { return `The feature toggle *${data.name}* was *unmarked* as stale`; } - generateArchivedText(event) { + generateArchivedText(event: IEvent): string { const { data, type } = event; const action = type === FEATURE_ARCHIVED ? 'archived' : 'revived'; return `The feature toggle *${data.name}* was *${action}*`; } - generateText(event) { + generateText(event: IEvent): string { const { data } = event; const typeStr = `*Type*: ${data.type}`; const project = `*Project*: ${data.project}`; @@ -113,7 +116,7 @@ class TeamsAddon extends Addon { return `Feature toggle ${data.name} | ${typeStr} | ${project}
${strategies}`; } - getAction(type) { + getAction(type: string): string { switch (type) { case FEATURE_CREATED: return 'Create'; @@ -124,5 +127,3 @@ class TeamsAddon extends Addon { } } } - -module.exports = TeamsAddon; diff --git a/src/lib/addons/webhook-definition.js b/src/lib/addons/webhook-definition.ts similarity index 88% rename from src/lib/addons/webhook-definition.js rename to src/lib/addons/webhook-definition.ts index 26a71c0379..563d147257 100644 --- a/src/lib/addons/webhook-definition.js +++ b/src/lib/addons/webhook-definition.ts @@ -1,13 +1,14 @@ -const { - FEATURE_CREATED, - FEATURE_UPDATED, +import { FEATURE_ARCHIVED, + FEATURE_CREATED, FEATURE_REVIVED, - FEATURE_STALE_ON, FEATURE_STALE_OFF, -} = require('../types/events'); + FEATURE_STALE_ON, + FEATURE_UPDATED, +} from '../types/events'; +import { IAddonDefinition } from '../types/model'; -module.exports = { +const webhookDefinition: IAddonDefinition = { name: 'webhook', displayName: 'Webhook', description: @@ -31,6 +32,7 @@ module.exports = { '(Optional) The Content-Type header to use. Defaults to "application/json".', type: 'text', required: false, + sensitive: false, }, { name: 'bodyTemplate', @@ -45,6 +47,7 @@ module.exports = { "(Optional) You may format the body using a mustache template. If you don't specify anything, the format will similar to the events format (https://docs.getunleash.io/docs/api/admin/events)", type: 'textfield', required: false, + sensitive: false, }, ], events: [ @@ -56,3 +59,5 @@ module.exports = { FEATURE_STALE_OFF, ], }; + +export default webhookDefinition; diff --git a/src/lib/addons/webhook.test.js b/src/lib/addons/webhook.test.ts similarity index 63% rename from src/lib/addons/webhook.test.js rename to src/lib/addons/webhook.test.ts index b3777510df..f81ff435b3 100644 --- a/src/lib/addons/webhook.test.js +++ b/src/lib/addons/webhook.test.ts @@ -1,28 +1,42 @@ -const { FEATURE_CREATED } = require('../types/events'); +import { Logger } from '../logger'; + +import { FEATURE_CREATED } from '../types/events'; + +import WebhookAddon from './webhook'; + +import noLogger from '../../test/fixtures/no-logger'; +import { IEvent } from '../types/model'; + +let fetchRetryCalls: any[] = []; jest.mock( './addon', () => class Addon { + logger: Logger; + constructor(definition, { getLogger }) { this.logger = getLogger('addon/test'); - this.fetchRetryCalls = []; + fetchRetryCalls = []; } async fetchRetry(url, options, retries, backoff) { - this.fetchRetryCalls.push({ url, options, retries, backoff }); + fetchRetryCalls.push({ + url, + options, + retries, + backoff, + }); return Promise.resolve({ status: 200 }); } }, ); -const WebhookAddon = require('./webhook'); - -const noLogger = require('../../test/fixtures/no-logger'); - test('Should handle event without "bodyTemplate"', () => { const addon = new WebhookAddon({ getLogger: noLogger }); - const event = { + const event: IEvent = { + id: 1, + createdAt: new Date(), type: FEATURE_CREATED, createdBy: 'some@user.com', data: { @@ -37,14 +51,16 @@ test('Should handle event without "bodyTemplate"', () => { }; addon.handleEvent(event, parameters); - expect(addon.fetchRetryCalls.length).toBe(1); - expect(addon.fetchRetryCalls[0].url).toBe(parameters.url); - expect(addon.fetchRetryCalls[0].options.body).toBe(JSON.stringify(event)); + expect(fetchRetryCalls.length).toBe(1); + expect(fetchRetryCalls[0].url).toBe(parameters.url); + expect(fetchRetryCalls[0].options.body).toBe(JSON.stringify(event)); }); test('Should format event with "bodyTemplate"', () => { const addon = new WebhookAddon({ getLogger: noLogger }); - const event = { + const event: IEvent = { + id: 1, + createdAt: new Date(), type: FEATURE_CREATED, createdBy: 'some@user.com', data: { @@ -61,8 +77,8 @@ test('Should format event with "bodyTemplate"', () => { }; addon.handleEvent(event, parameters); - const call = addon.fetchRetryCalls[0]; - expect(addon.fetchRetryCalls.length).toBe(1); + const call = fetchRetryCalls[0]; + expect(fetchRetryCalls.length).toBe(1); expect(call.url).toBe(parameters.url); expect(call.options.headers['Content-Type']).toBe('text/plain'); expect(call.options.body).toBe('feature-created on toggle some-toggle'); diff --git a/src/lib/addons/webhook.js b/src/lib/addons/webhook.ts similarity index 62% rename from src/lib/addons/webhook.js rename to src/lib/addons/webhook.ts index 60b1645dce..22c4874c26 100644 --- a/src/lib/addons/webhook.js +++ b/src/lib/addons/webhook.ts @@ -1,15 +1,16 @@ -'use strict'; +import Mustache from 'mustache'; +import Addon from './addon'; +import definition from './webhook-definition'; +import { LogProvider } from '../logger'; +import { IEvent } from '../types/model'; -const Mustache = require('mustache'); -const Addon = require('./addon'); -const definition = require('./webhook-definition'); - -class Webhook extends Addon { - constructor(args) { +export default class Webhook extends Addon { + constructor(args: { getLogger: LogProvider }) { super(definition, args); } - async handleEvent(event, parameters) { + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + async handleEvent(event: IEvent, parameters: any): Promise { const { url, bodyTemplate, contentType } = parameters; const context = { event, @@ -35,5 +36,3 @@ class Webhook extends Addon { ); } } - -module.exports = Webhook; diff --git a/src/lib/app.ts b/src/lib/app.ts index 3888c69f12..09e93d80b7 100644 --- a/src/lib/app.ts +++ b/src/lib/app.ts @@ -1,7 +1,7 @@ import { publicFolder } from 'unleash-frontend'; import fs from 'fs'; import EventEmitter from 'events'; -import express, { Application } from 'express'; +import express, { Application, RequestHandler } from 'express'; import cors from 'cors'; import compression from 'compression'; import favicon from 'serve-favicon'; @@ -14,7 +14,6 @@ import apiTokenMiddleware from './middleware/api-token-middleware'; import { IUnleashServices } from './types/services'; import { IAuthType, IUnleashConfig } from './types/option'; import { IUnleashStores } from './types/stores'; -import unleashDbSession from './middleware/session-db'; import IndexRouter from './routes'; @@ -23,6 +22,7 @@ import demoAuthentication from './middleware/demo-authentication'; import ossAuthentication from './middleware/oss-authentication'; import noAuthentication from './middleware/no-authentication'; import secureHeaders from './middleware/secure-headers'; + import { rewriteHTML } from './util/rewriteHTML'; export default function getApp( @@ -30,6 +30,7 @@ export default function getApp( stores: IUnleashStores, services: IUnleashServices, eventBus?: EventEmitter, + unleashSession?: RequestHandler, ): Application { const app = express(); @@ -63,7 +64,9 @@ export default function getApp( app.use(compression()); app.use(cookieParser()); app.use(express.json({ strict: false })); - app.use(unleashDbSession(config, stores)); + if (unleashSession) { + app.use(unleashSession); + } app.use(secureHeaders(config)); app.use(express.urlencoded({ extended: true })); app.use(favicon(path.join(publicFolder, 'favicon.ico'))); @@ -76,7 +79,7 @@ export default function getApp( switch (config.authentication.type) { case IAuthType.OPEN_SOURCE: { app.use(baseUriPath, apiTokenMiddleware(config, services)); - ossAuthentication(app, config); + ossAuthentication(app, config.server.baseUriPath); break; } case IAuthType.ENTERPRISE: { diff --git a/src/lib/create-config.ts b/src/lib/create-config.ts index 138893bbd7..c0c897676f 100644 --- a/src/lib/create-config.ts +++ b/src/lib/create-config.ts @@ -48,7 +48,7 @@ function safeBoolean(envVar, defaultVal) { } function mergeAll(objects: Partial[]): T { - return merge.all(objects.filter(i => i)); + return merge.all(objects.filter((i) => i)); } const defaultDbOptions: IDBOption = { diff --git a/src/lib/db/access-store.ts b/src/lib/db/access-store.ts index cf4a0c8b14..cee7c16fb9 100644 --- a/src/lib/db/access-store.ts +++ b/src/lib/db/access-store.ts @@ -2,6 +2,13 @@ import { EventEmitter } from 'events'; import { Knex } from 'knex'; import metricsHelper from '../util/metrics-helper'; import { DB_TIME } from '../metric-events'; +import { Logger } from '../logger'; +import { + IAccessStore, + IRole, + IUserPermission, + IUserRole, +} from '../types/stores/access-store'; const T = { ROLE_USER: 'role_user', @@ -9,26 +16,8 @@ const T = { ROLE_PERMISSION: 'role_permission', }; -export interface IUserPermission { - project?: string; - permission: string; -} - -export interface IRole { - id: number; - name: string; - description?: string; - type: string; - project?: string; -} - -export interface IUserRole { - roleId: number; - userId: number; -} - -export class AccessStore { - private logger: Function; +export class AccessStore implements IAccessStore { + private logger: Logger; private timer: Function; @@ -36,7 +25,7 @@ export class AccessStore { constructor(db: Knex, eventBus: EventEmitter, getLogger: Function) { this.db = db; - this.logger = getLogger('access-store.js'); + this.logger = getLogger('access-store.ts'); this.timer = (action: string) => metricsHelper.wrapTimer(eventBus, DB_TIME, { store: 'access-store', @@ -44,6 +33,37 @@ export class AccessStore { }); } + async delete(key: number): Promise { + await this.db(T.ROLES).where({ id: key }).del(); + } + + async deleteAll(): Promise { + await this.db(T.ROLES).del(); + } + + destroy(): void {} + + async exists(key: number): Promise { + const result = await this.db.raw( + `SELECT EXISTS (SELECT 1 FROM ${T.ROLES} WHERE id = ?) AS present`, + [key], + ); + const { present } = result.rows[0]; + return present; + } + + async get(key: number): Promise { + return this.db + .select(['id', 'name', 'type', 'description']) + .where('id', key) + .first() + .from(T.ROLES); + } + + async getAll(): Promise { + return Promise.resolve([]); + } + async getPermissionsForUser(userId: number): Promise { const stopTimer = this.timer('getPermissionsForUser'); const rows = await this.db @@ -110,12 +130,12 @@ export class AccessStore { .where('ru.user_id', '=', userId); } - async getUserIdsForRole(roleId: number): Promise { + async getUserIdsForRole(roleId: number): Promise { const rows = await this.db .select(['user_id']) .from(T.ROLE_USER) .where('role_id', roleId); - return rows.map(r => r.user_id); + return rows.map((r) => r.user_id); } async addUserToRole(userId: number, roleId: number): Promise { @@ -155,9 +175,20 @@ export class AccessStore { description?: string, ): Promise { const [id] = await this.db(T.ROLES) - .insert({ name, description, type, project }) + .insert({ + name, + description, + type, + project, + }) .returning('id'); - return { id, name, description, type, project }; + return { + id, + name, + description, + type, + project, + }; } async addPermissionsToRole( @@ -165,7 +196,7 @@ export class AccessStore { permissions: string[], projectId?: string, ): Promise { - const rows = permissions.map(permission => ({ + const rows = permissions.map((permission) => ({ role_id, project: projectId, permission, @@ -195,7 +226,7 @@ export class AccessStore { .leftJoin(`${T.ROLE_USER} AS ru`, 'r.id', 'ru.role_id') .where('r.type', '=', 'root'); - return rows.map(row => ({ + return rows.map((row) => ({ roleId: +row.id, userId: +row.user_id, })); diff --git a/src/lib/db/addon-store.js b/src/lib/db/addon-store.js deleted file mode 100644 index 9a2e8dbc89..0000000000 --- a/src/lib/db/addon-store.js +++ /dev/null @@ -1,105 +0,0 @@ -'use strict'; - -const metricsHelper = require('../util/metrics-helper'); -const { DB_TIME } = require('../metric-events'); -const NotFoundError = require('../error/notfound-error'); - -const COLUMNS = [ - 'id', - 'provider', - 'enabled', - 'description', - 'parameters', - 'events', -]; -const TABLE = 'addons'; - -class AddonStore { - constructor(db, eventBus, getLogger) { - this.db = db; - this.logger = getLogger('addons-store.js'); - this.timer = action => - metricsHelper.wrapTimer(eventBus, DB_TIME, { - store: 'addons', - action, - }); - } - - async getAll(query = {}) { - const stopTimer = this.timer('getAll'); - const rows = await this.db - .select(COLUMNS) - .where(query) - .from(TABLE); - stopTimer(); - return rows.map(this.rowToAddon); - } - - async get(id) { - const stopTimer = this.timer('get'); - return this.db - .first(COLUMNS) - .from(TABLE) - .where({ id }) - .then(row => { - stopTimer(); - if (!row) { - throw new NotFoundError('Could not find addon'); - } else { - return this.rowToAddon(row); - } - }); - } - - async insert(addon) { - const stopTimer = this.timer('insert'); - const [id] = await this.db(TABLE).insert(this.addonToRow(addon), 'id'); - stopTimer(); - return { id, ...addon }; - } - - async update(id, addon) { - const rows = await this.db(TABLE) - .where({ id }) - .update(this.addonToRow(addon)); - - if (!rows) { - throw new NotFoundError('Could not find addon'); - } - return rows; - } - - async delete(id) { - const rows = await this.db(TABLE) - .where({ id }) - .del(); - - if (!rows) { - throw new NotFoundError('Could not find addon'); - } - return rows; - } - - rowToAddon(row) { - return { - id: row.id, - provider: row.provider, - enabled: row.enabled, - description: row.description, - parameters: row.parameters, - events: row.events, - }; - } - - addonToRow(addon) { - return { - provider: addon.provider, - enabled: addon.enabled, - description: addon.description, - parameters: JSON.stringify(addon.parameters), - events: JSON.stringify(addon.events), - }; - } -} - -module.exports = AddonStore; diff --git a/src/lib/db/addon-store.ts b/src/lib/db/addon-store.ts new file mode 100644 index 0000000000..419a17903a --- /dev/null +++ b/src/lib/db/addon-store.ts @@ -0,0 +1,132 @@ +import { Knex } from 'knex'; +import EventEmitter from 'events'; +import { Logger, LogProvider } from '../logger'; +import { IAddon, IAddonDto, IAddonStore } from '../types/stores/addon-store'; + +import metricsHelper from '../util/metrics-helper'; +import { DB_TIME } from '../metric-events'; +import NotFoundError from '../error/notfound-error'; + +const COLUMNS = [ + 'id', + 'provider', + 'enabled', + 'description', + 'parameters', + 'events', +]; +const TABLE = 'addons'; + +export default class AddonStore implements IAddonStore { + private db: Knex; + + private logger: Logger; + + private readonly timer: Function; + + constructor(db: Knex, eventBus: EventEmitter, getLogger: LogProvider) { + this.db = db; + this.logger = getLogger('addons-store.ts'); + this.timer = (action) => + metricsHelper.wrapTimer(eventBus, DB_TIME, { + store: 'addons', + action, + }); + } + + destroy(): void {} + + async getAll(query = {}): Promise { + const stopTimer = this.timer('getAll'); + const rows = await this.db.select(COLUMNS).where(query).from(TABLE); + stopTimer(); + return rows.map(this.rowToAddon); + } + + async get(id: number): Promise { + const stopTimer = this.timer('get'); + return this.db + .first(COLUMNS) + .from(TABLE) + .where({ id }) + .then((row) => { + stopTimer(); + if (!row) { + throw new NotFoundError('Could not find addon'); + } else { + return this.rowToAddon(row); + } + }); + } + + async insert(addon: IAddonDto): Promise { + const stopTimer = this.timer('insert'); + // eslint-disable-next-line @typescript-eslint/naming-convention + const rows = await this.db(TABLE).insert(this.addonToRow(addon), [ + 'id', + 'created_at', + ]); + stopTimer(); + // eslint-disable-next-line @typescript-eslint/naming-convention + const { id, created_at } = rows[0]; + return { id, createdAt: created_at, ...addon }; + } + + async update(id: number, addon: IAddonDto): Promise { + const rows = await this.db(TABLE) + .where({ id }) + .update(this.addonToRow(addon)); + + if (!rows) { + throw new NotFoundError('Could not find addon'); + } + return rows[0]; + } + + async delete(id: number): Promise { + const rows = await this.db(TABLE).where({ id }).del(); + + if (!rows) { + throw new NotFoundError('Could not find addon'); + } + } + + async deleteAll(): Promise { + await this.db(TABLE).del(); + } + + async exists(id: number): Promise { + const stopTimer = this.timer('exists'); + const result = await this.db.raw( + `SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE id = ?) AS present`, + [id], + ); + const { present } = result.rows[0]; + stopTimer(); + return present; + } + + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + rowToAddon(row): IAddon { + return { + id: row.id, + provider: row.provider, + enabled: row.enabled, + description: row.description, + parameters: row.parameters, + events: row.events, + createdAt: row.created_at, + }; + } + + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + addonToRow(addon: IAddonDto) { + return { + provider: addon.provider, + enabled: addon.enabled, + description: addon.description, + parameters: JSON.stringify(addon.parameters), + events: JSON.stringify(addon.events), + }; + } +} diff --git a/src/lib/db/api-token-store.ts b/src/lib/db/api-token-store.ts index 22c38efb94..b6ecec070a 100644 --- a/src/lib/db/api-token-store.ts +++ b/src/lib/db/api-token-store.ts @@ -4,6 +4,12 @@ import metricsHelper from '../util/metrics-helper'; import { DB_TIME } from '../metric-events'; import { Logger, LogProvider } from '../logger'; import NotFoundError from '../error/notfound-error'; +import { + ApiTokenType, + IApiToken, + IApiTokenCreate, + IApiTokenStore, +} from '../types/stores/api-token-store'; const TABLE = 'api_tokens'; @@ -17,23 +23,6 @@ interface ITokenTable { seen_at?: Date; } -export enum ApiTokenType { - CLIENT = 'client', - ADMIN = 'admin', -} - -export interface IApiTokenCreate { - secret: string; - username: string; - type: ApiTokenType; - expiresAt?: Date; -} - -export interface IApiToken extends IApiTokenCreate { - createdAt: Date; - seenAt?: Date; -} - const toRow = (newToken: IApiTokenCreate) => ({ username: newToken.username, secret: newToken.secret, @@ -49,7 +38,7 @@ const toToken = (row: ITokenTable): IApiToken => ({ createdAt: row.created_at, }); -export class ApiTokenStore { +export class ApiTokenStore implements IApiTokenStore { private logger: Logger; private timer: Function; @@ -90,10 +79,24 @@ export class ApiTokenStore { return { ...newToken, createdAt: row.created_at }; } + destroy(): void {} + + async exists(secret: string): Promise { + const result = await this.db.raw( + `SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE secret = ?) AS present`, + [secret], + ); + const { present } = result.rows[0]; + return present; + } + + async get(key: string): Promise { + const row = await this.db(TABLE).where('secret', key).first(); + return toToken(row); + } + async delete(secret: string): Promise { - return this.db(TABLE) - .where({ secret }) - .del(); + return this.db(TABLE).where({ secret }).del(); } async deleteAll(): Promise { diff --git a/src/lib/db/client-applications-store.js b/src/lib/db/client-applications-store.ts similarity index 53% rename from src/lib/db/client-applications-store.js rename to src/lib/db/client-applications-store.ts index d3bbe80733..a5eabbb85f 100644 --- a/src/lib/db/client-applications-store.js +++ b/src/lib/db/client-applications-store.ts @@ -1,6 +1,12 @@ -/* eslint camelcase:off */ - -const NotFoundError = require('../error/notfound-error'); +import EventEmitter from 'events'; +import { Knex } from 'knex'; +import NotFoundError from '../error/notfound-error'; +import { + IClientApplication, + IClientApplicationsStore, +} from '../types/stores/client-applications-store'; +import { Logger, LogProvider } from '../logger'; +import { IApplicationQuery } from '../types/query'; const COLUMNS = [ 'app_name', @@ -15,7 +21,7 @@ const COLUMNS = [ ]; const TABLE = 'client_applications'; -const mapRow = row => ({ +const mapRow: (any) => IClientApplication = (row) => ({ appName: row.app_name, createdAt: row.created_at, updatedAt: row.updated_at, @@ -25,9 +31,11 @@ const mapRow = row => ({ url: row.url, color: row.color, icon: row.icon, + lastSeen: row.last_seen, + announced: row.announced, }); -const remapRow = input => { +const remapRow = (input) => { const temp = { app_name: input.appName, updated_at: input.updatedAt || new Date(), @@ -40,7 +48,7 @@ const remapRow = input => { icon: input.icon, strategies: JSON.stringify(input.strategies), }; - Object.keys(temp).forEach(k => { + Object.keys(temp).forEach((k) => { if (temp[k] === undefined) { // not using !temp[k] to allow false and null values to get through delete temp[k]; @@ -49,38 +57,38 @@ const remapRow = input => { return temp; }; -class ClientApplicationsDb { - constructor(db, eventBus) { +export default class ClientApplicationsStore + implements IClientApplicationsStore +{ + private db: Knex; + + private logger: Logger; + + constructor(db: Knex, eventBus: EventEmitter, getLogger: LogProvider) { this.db = db; - this.eventBus = eventBus; + this.logger = getLogger('client-applications-store.ts'); } - async upsert(details) { + async upsert(details: Partial): Promise { const row = remapRow(details); - return this.db(TABLE) - .insert(row) - .onConflict('app_name') - .merge(); + await this.db(TABLE).insert(row).onConflict('app_name').merge(); } - async bulkUpsert(apps) { + async bulkUpsert(apps: Partial[]): Promise { const rows = apps.map(remapRow); - return this.db(TABLE) - .insert(rows) - .onConflict('app_name') - .merge(); + await this.db(TABLE).insert(rows).onConflict('app_name').merge(); } - async exists({ appName }) { + async exists(appName: string): Promise { const result = await this.db.raw( - `SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE app_name = ?) AS present`, + `SELECT EXISTS(SELECT 1 FROM ${TABLE} WHERE app_name = ?) AS present`, [appName], ); const { present } = result.rows[0]; return present; } - async getAll() { + async getAll(): Promise { const rows = await this.db .select(COLUMNS) .from(TABLE) @@ -89,7 +97,7 @@ class ClientApplicationsDb { return rows.map(mapRow); } - async getApplication(appName) { + async getApplication(appName: string): Promise { const row = await this.db .select(COLUMNS) .where('app_name', appName) @@ -103,10 +111,8 @@ class ClientApplicationsDb { return mapRow(row); } - async deleteApplication(appName) { - return this.db(TABLE) - .where('app_name', appName) - .del(); + async deleteApplication(appName: string): Promise { + return this.db(TABLE).where('app_name', appName).del(); } /** @@ -118,23 +124,21 @@ class ClientApplicationsDb { * ) as foo * WHERE foo.strategyName = '"other"'; */ - async getAppsForStrategy(strategyName) { + async getAppsForStrategy( + query: IApplicationQuery, + ): Promise { const rows = await this.db.select(COLUMNS).from(TABLE); + const apps = rows.map(mapRow); - return rows - .map(mapRow) - .filter(apps => - apps.filter(app => app.strategies.includes(strategyName)), + if (query.strategyName) { + return apps.filter((app) => + app.strategies.includes(query.strategyName), ); + } + return apps; } - async getApplications(filter) { - return filter && filter.strategyName - ? this.getAppsForStrategy(filter.strategyName) - : this.getAll(); - } - - async getUnannounced() { + async getUnannounced(): Promise { const rows = await this.db(TABLE) .select(COLUMNS) .where('announced', false); @@ -145,7 +149,7 @@ class ClientApplicationsDb { * Updates all rows that have announced = false to announced =true and returns the rows altered * @return {[app]} - Apps that hadn't been announced */ - async setUnannouncedToAnnounced() { + async setUnannouncedToAnnounced(): Promise { const rows = await this.db(TABLE) .update({ announced: true }) .where('announced', false) @@ -153,6 +157,28 @@ class ClientApplicationsDb { .returning(COLUMNS); return rows.map(mapRow); } -} -module.exports = ClientApplicationsDb; + async delete(key: string): Promise { + await this.db(TABLE).where('app_name', key).del(); + } + + async deleteAll(): Promise { + await this.db(TABLE).del(); + } + + destroy(): void {} + + async get(appName: string): Promise { + const row = await this.db + .select(COLUMNS) + .where('app_name', appName) + .from(TABLE) + .first(); + + if (!row) { + throw new NotFoundError(`Could not find appName=${appName}`); + } + + return mapRow(row); + } +} diff --git a/src/lib/db/client-instance-store.js b/src/lib/db/client-instance-store.ts similarity index 55% rename from src/lib/db/client-instance-store.js rename to src/lib/db/client-instance-store.ts index 8c6263de18..67df244682 100644 --- a/src/lib/db/client-instance-store.js +++ b/src/lib/db/client-instance-store.ts @@ -1,6 +1,12 @@ -/* eslint camelcase: "off" */ - -'use strict'; +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'; const metricsHelper = require('../util/metrics-helper'); const { DB_TIME } = require('../metric-events'); @@ -17,7 +23,7 @@ const TABLE = 'client_instances'; const ONE_DAY = 24 * 61 * 60 * 1000; -const mapRow = row => ({ +const mapRow = (row) => ({ appName: row.app_name, instanceId: row.instance_id, sdkVersion: row.sdk_version, @@ -26,7 +32,7 @@ const mapRow = row => ({ createdAt: row.created_at, }); -const mapToDb = client => ({ +const mapToDb = (client) => ({ app_name: client.appName, instance_id: client.instanceId, sdk_version: client.sdkVersion || '', @@ -34,12 +40,22 @@ const mapToDb = client => ({ last_seen: client.lastSeen || 'now()', }); -class ClientInstanceStore { - constructor(db, eventBus, getLogger) { +export default class ClientInstanceStore implements IClientInstanceStore { + private db: Knex; + + private logger: Logger; + + private eventBus: EventEmitter; + + private metricTimer: Function; + + private timer: Timeout; + + constructor(db: Knex, eventBus: EventEmitter, getLogger: LogProvider) { this.db = db; this.eventBus = eventBus; - this.logger = getLogger('client-instance-store.js'); - this.metricTimer = action => + this.logger = getLogger('client-instance-store.ts'); + this.metricTimer = (action) => metricsHelper.wrapTimer(eventBus, DB_TIME, { store: 'instance', action, @@ -49,7 +65,7 @@ class ClientInstanceStore { this.timer = setInterval(clearer, ONE_DAY).unref(); } - async _removeInstancesOlderThanTwoDays() { + async _removeInstancesOlderThanTwoDays(): Promise { const rows = await this.db(TABLE) .whereRaw("created_at < now() - interval '2 days'") .del(); @@ -59,15 +75,44 @@ class ClientInstanceStore { } } - async bulkUpsert(instances) { + async bulkUpsert(instances: INewClientInstance[]): Promise { const rows = instances.map(mapToDb); - return this.db(TABLE) + await this.db(TABLE) .insert(rows) .onConflict(['app_name', 'instance_id']) .merge(); } - async exists({ appName, instanceId }) { + async delete({ + appName, + instanceId, + }: Pick): Promise { + await this.db(TABLE) + .where({ app_name: appName, instance_id: instanceId }) + .del(); + } + + async deleteAll(): Promise { + await this.db(TABLE).del(); + } + + async get({ + appName, + instanceId, + }: Pick< + INewClientInstance, + 'appName' | 'instanceId' + >): Promise { + const row = await this.db(TABLE) + .where({ app_name: appName, instance_id: instanceId }) + .first(); + return mapRow(row); + } + + async exists({ + appName, + instanceId, + }: Pick): Promise { const result = await this.db.raw( `SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE app_name = ? AND instance_id = ?) AS present`, [appName, instanceId], @@ -76,20 +121,18 @@ class ClientInstanceStore { return present; } - async insert(details) { + async insert(details: INewClientInstance): Promise { const stopTimer = this.metricTimer('insert'); - const item = await this.db(TABLE) + await this.db(TABLE) .insert(mapToDb(details)) .onConflict(['app_name', 'instance_id']) .merge(); stopTimer(); - - return item; } - async getAll() { + async getAll(): Promise { const stopTimer = this.metricTimer('getAll'); const rows = await this.db @@ -104,7 +147,7 @@ class ClientInstanceStore { return toggles; } - async getByAppName(appName) { + async getByAppName(appName: string): Promise { const rows = await this.db .select() .from(TABLE) @@ -114,25 +157,21 @@ class ClientInstanceStore { return rows.map(mapRow); } - async getApplications() { + async getDistinctApplications(): Promise { const rows = await this.db .distinct('app_name') .select(['app_name']) .from(TABLE) .orderBy('app_name', 'desc'); - return rows.map(mapRow); + return rows.map((r) => r.app_name); } - async deleteForApplication(appName) { - return this.db(TABLE) - .where('app_name', appName) - .del(); + async deleteForApplication(appName: string): Promise { + return this.db(TABLE).where('app_name', appName).del(); } - destroy() { + destroy(): void { clearInterval(this.timer); } } - -module.exports = ClientInstanceStore; diff --git a/src/lib/db/client-metrics-db.ts b/src/lib/db/client-metrics-db.ts index 81d718ce82..a40c85636a 100644 --- a/src/lib/db/client-metrics-db.ts +++ b/src/lib/db/client-metrics-db.ts @@ -1,23 +1,18 @@ import { Knex } from 'knex'; import { Logger, LogProvider } from '../logger'; +import { IClientMetric } from '../types/stores/client-metrics-db'; const METRICS_COLUMNS = ['id', 'created_at', 'metrics']; const TABLE = 'client_metrics'; const ONE_MINUTE = 60 * 1000; -const mapRow = row => ({ +const mapRow = (row) => ({ id: row.id, createdAt: row.created_at, metrics: row.metrics, }); -export interface IClientMetric { - id: number; - createdAt: Date; - metrics: any; -} - export class ClientMetricsDb { private readonly logger: Logger; @@ -45,6 +40,14 @@ export class ClientMetricsDb { } } + async delete(id: number): Promise { + await this.db(TABLE).where({ id }).del(); + } + + async deleteAll(): Promise { + await this.db(TABLE).del(); + } + // Insert new client metrics async insert(metrics: IClientMetric): Promise { return this.db(TABLE).insert({ metrics }); @@ -66,6 +69,24 @@ export class ClientMetricsDb { return []; } + async get(id: number): Promise { + const result = await this.db + .select(METRICS_COLUMNS) + .from(TABLE) + .where({ id }) + .first(); + return mapRow(result); + } + + async exists(id: number): Promise { + const result = await this.db.raw( + `SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE id = ?) AS present`, + [id], + ); + const { present } = result.rows[0]; + return present; + } + // Used to poll for new metrics async getNewMetrics(lastKnownId: number): Promise { try { diff --git a/src/lib/db/client-metrics-store.test.ts b/src/lib/db/client-metrics-store.test.ts index 040b3bd661..c6d7483c17 100644 --- a/src/lib/db/client-metrics-store.test.ts +++ b/src/lib/db/client-metrics-store.test.ts @@ -23,7 +23,7 @@ function getMockDb() { }; } -test('should call database on startup', done => { +test('should call database on startup', (done) => { jest.useFakeTimers('modern'); const mock = getMockDb(); const ee = new EventEmitter(); @@ -33,7 +33,7 @@ test('should call database on startup', done => { expect.assertions(2); - store.on('metrics', metrics => { + store.on('metrics', (metrics) => { expect(store.highestIdSeen).toBe(1); expect(metrics.appName).toBe('test'); store.destroy(); @@ -42,7 +42,7 @@ test('should call database on startup', done => { }); }); -test('should start poller even if initial database fetch fails', done => { +test('should start poller even if initial database fetch fails', (done) => { jest.useFakeTimers('modern'); getLogger.setMuteError(true); const mock = getMockDb(); @@ -52,7 +52,7 @@ test('should start poller even if initial database fetch fails', done => { jest.runAllTicks(); const metrics = []; - store.on('metrics', m => metrics.push(m)); + store.on('metrics', (m) => metrics.push(m)); store.on('ready', () => { jest.useFakeTimers('modern'); @@ -69,7 +69,7 @@ test('should start poller even if initial database fetch fails', done => { getLogger.setMuteError(false); }); -test('should poll for updates', done => { +test('should poll for updates', (done) => { jest.useFakeTimers('modern'); const mock = getMockDb(); const ee = new EventEmitter(); @@ -77,7 +77,7 @@ test('should poll for updates', done => { jest.runAllTicks(); const metrics = []; - store.on('metrics', m => metrics.push(m)); + store.on('metrics', (m) => metrics.push(m)); expect(metrics).toHaveLength(0); diff --git a/src/lib/db/client-metrics-store.ts b/src/lib/db/client-metrics-store.ts index 5aaf7523fb..7f39b3afc2 100644 --- a/src/lib/db/client-metrics-store.ts +++ b/src/lib/db/client-metrics-store.ts @@ -1,14 +1,17 @@ -'use strict'; - import EventEmitter from 'events'; -import { ClientMetricsDb, IClientMetric } from './client-metrics-db'; import { Logger, LogProvider } from '../logger'; import metricsHelper from '../util/metrics-helper'; 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; -export class ClientMetricsStore extends EventEmitter { +export class ClientMetricsStore + extends EventEmitter + implements IClientMetricsStore +{ private logger: Logger; highestIdSeen = 0; @@ -24,11 +27,11 @@ export class ClientMetricsStore extends EventEmitter { pollInterval = TEN_SECONDS, ) { super(); - this.logger = getLogger('client-metrics-store.js'); + this.logger = getLogger('client-metrics-store.ts.js'); this.metricsDb = metricsDb; this.highestIdSeen = 0; - this.startTimer = action => + this.startTimer = (action) => metricsHelper.wrapTimer(eventBus, DB_TIME, { store: 'metrics', action, @@ -58,13 +61,13 @@ export class ClientMetricsStore extends EventEmitter { _fetchNewAndEmit(): void { this.metricsDb .getNewMetrics(this.highestIdSeen) - .then(metrics => this._emitMetrics(metrics)); + .then((metrics) => this._emitMetrics(metrics)); } _emitMetrics(metrics: IClientMetric[]): void { if (metrics && metrics.length > 0) { this.highestIdSeen = metrics[metrics.length - 1].id; - metrics.forEach(m => this.emit('metrics', m.metrics)); + metrics.forEach((m) => this.emit('metrics', m.metrics)); } } @@ -81,4 +84,24 @@ export class ClientMetricsStore extends EventEmitter { clearInterval(this.timer); this.metricsDb.destroy(); } + + async delete(key: number): Promise { + await this.metricsDb.delete(key); + } + + async deleteAll(): Promise { + await this.metricsDb.deleteAll(); + } + + async exists(key: number): Promise { + return this.metricsDb.exists(key); + } + + async get(key: number): Promise { + return this.metricsDb.get(key); + } + + async getAll(): Promise { + return this.metricsDb.getMetricsLastHour(); + } } diff --git a/src/lib/db/context-field-store.ts b/src/lib/db/context-field-store.ts index c0b01657ee..89e77f024e 100644 --- a/src/lib/db/context-field-store.ts +++ b/src/lib/db/context-field-store.ts @@ -1,5 +1,9 @@ import { Knex } from 'knex'; import { Logger, LogProvider } from '../logger'; +import { + IContextField, + IContextFieldStore, +} from '../types/stores/context-field-store'; const COLUMNS = [ 'name', @@ -11,7 +15,7 @@ const COLUMNS = [ ]; const TABLE = 'context_fields'; -const mapRow: (IContextRow) => IContextField = row => ({ +const mapRow: (IContextRow) => IContextField = (row) => ({ name: row.name, description: row.description, stickiness: row.stickiness, @@ -20,7 +24,7 @@ const mapRow: (IContextRow) => IContextField = row => ({ createdAt: row.created_at, }); -export interface ICreateContextField { +interface ICreateContextField { name: string; description: string; stickiness: boolean; @@ -29,23 +33,14 @@ export interface ICreateContextField { updated_at: Date; } -export interface IContextField { - name: string; - description: string; - stickiness: boolean; - sortOrder: number; - legalValues?: string[]; - createdAt: Date; -} - -class ContextFieldStore { +class ContextFieldStore implements IContextFieldStore { private db: Knex; private logger: Logger; constructor(db: Knex, getLogger: LogProvider) { this.db = db; - this.logger = getLogger('context-field-store.js'); + this.logger = getLogger('context-field-store.ts'); } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types @@ -69,30 +64,48 @@ class ContextFieldStore { return rows.map(mapRow); } - async get(name: string): Promise { + async get(key: string): Promise { return this.db .first(COLUMNS) .from(TABLE) - .where({ name }) + .where({ name: key }) .then(mapRow); } - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - async create(contextField): Promise { - return this.db(TABLE).insert(this.fieldToRow(contextField)); + async deleteAll(): Promise { + await this.db(TABLE).del(); + } + + destroy(): void {} + + async exists(key: string): Promise { + const result = await this.db.raw( + `SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE name = ?) AS present`, + [key], + ); + const { present } = result.rows[0]; + return present; } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - async update(data): Promise { - return this.db(TABLE) + async create(contextField): Promise { + const row = await this.db(TABLE) + .insert(this.fieldToRow(contextField)) + .returning('*'); + return mapRow(row); + } + + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + async update(data): Promise { + const row = this.db(TABLE) .where({ name: data.name }) - .update(this.fieldToRow(data)); + .update(this.fieldToRow(data)) + .returning('*'); + return mapRow(row); } async delete(name: string): Promise { - return this.db(TABLE) - .where({ name }) - .del(); + return this.db(TABLE).where({ name }).del(); } } export default ContextFieldStore; diff --git a/src/lib/db/db-pool.ts b/src/lib/db/db-pool.ts index 8cbd96a4f8..0bcb943f9d 100644 --- a/src/lib/db/db-pool.ts +++ b/src/lib/db/db-pool.ts @@ -14,9 +14,9 @@ export function createDb({ searchPath: db.schema, asyncStackTraces: true, log: { - debug: msg => logger.debug(msg), - warn: msg => logger.warn(msg), - error: msg => logger.error(msg), + debug: (msg) => logger.debug(msg), + warn: (msg) => logger.warn(msg), + error: (msg) => logger.error(msg), }, }); } diff --git a/src/lib/db/environment-store.ts b/src/lib/db/environment-store.ts index c1cbe62fc2..6e23c638f0 100644 --- a/src/lib/db/environment-store.ts +++ b/src/lib/db/environment-store.ts @@ -5,6 +5,7 @@ import metricsHelper from '../util/metrics-helper'; import { DB_TIME } from '../metric-events'; import { IEnvironment } from '../types/model'; import NotFoundError from '../error/notfound-error'; +import { IEnvironmentStore } from '../types/stores/environment-store'; interface IEnvironmentsTable { name: string; @@ -34,7 +35,7 @@ function mapInput(input: IEnvironment): IEnvironmentsTable { const TABLE = 'environments'; -export default class EnvironmentStore { +export default class EnvironmentStore implements IEnvironmentStore { private logger: Logger; private db: Knex; @@ -44,19 +45,33 @@ export default class EnvironmentStore { constructor(db: Knex, eventBus: EventEmitter, getLogger: LogProvider) { this.db = db; this.logger = getLogger('db/environment-store.ts'); - this.timer = action => + this.timer = (action) => metricsHelper.wrapTimer(eventBus, DB_TIME, { store: 'environment', action, }); } + async deleteAll(): Promise { + await this.db(TABLE).del(); + } + + async get(key: string): Promise { + const row = await this.db(TABLE) + .where({ name: key }) + .first(); + if (row) { + return mapRow(row); + } + throw new NotFoundError(`Could not find environment with name: ${key}`); + } + async getAll(): Promise { const rows = await this.db(TABLE).select('*'); return rows.map(mapRow); } - async exists(name: string): Promise { + async exists(name: string): Promise { const result = await this.db.raw( `SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE name = ?) AS present`, [name], @@ -104,7 +119,7 @@ export default class EnvironmentStore { .where({ project: projectId, }); - const rows: IFeatureEnvironmentRow[] = featuresToEnable.map(f => ({ + const rows: IFeatureEnvironmentRow[] = featuresToEnable.map((f) => ({ environment, feature_name: f.name, enabled: false, @@ -118,9 +133,7 @@ export default class EnvironmentStore { } async delete(name: string): Promise { - await this.db(TABLE) - .where({ name }) - .del(); + await this.db(TABLE).where({ name }).del(); } async disconnectProjectFromEnv( @@ -140,7 +153,7 @@ export default class EnvironmentStore { .select('environment_name') .where({ project_id }); await Promise.all( - environmentsToEnable.map(async env => { + environmentsToEnable.map(async (env) => { await this.db('feature_environments') .insert({ environment: env.environment_name, @@ -152,4 +165,6 @@ export default class EnvironmentStore { }), ); } + + destroy(): void {} } diff --git a/src/lib/db/event-store.test.ts b/src/lib/db/event-store.test.ts index b96a5fa0a7..d737caf8ab 100644 --- a/src/lib/db/event-store.test.ts +++ b/src/lib/db/event-store.test.ts @@ -16,7 +16,7 @@ test('Trying to get events by name if db fails should yield empty list', async ( client: 'pg', }); const store = new EventStore(db, getLogger); - const events = await store.getEventsFilterByName('application-created'); + const events = await store.getEventsFilterByType('application-created'); expect(events).toBeTruthy(); expect(events.length).toBe(0); }); diff --git a/src/lib/db/event-store.ts b/src/lib/db/event-store.ts index 86fdf4e93c..8ef3c8800f 100644 --- a/src/lib/db/event-store.ts +++ b/src/lib/db/event-store.ts @@ -2,7 +2,8 @@ import { EventEmitter } from 'events'; import { Knex } from 'knex'; import { DROP_FEATURES } from '../types/events'; import { LogProvider, Logger } from '../logger'; -import { ITag } from '../types/model'; +import { IEventStore } from '../types/stores/event-store'; +import { ICreateEvent, IEvent } from '../types/model'; const EVENT_COLUMNS = [ 'id', @@ -13,7 +14,7 @@ const EVENT_COLUMNS = [ 'tags', ]; -interface IEventTable { +export interface IEventTable { id: number; type: string; created_by: string; @@ -22,21 +23,9 @@ interface IEventTable { tags: []; } -export interface ICreateEvent { - type: string; - createdBy: string; - data?: any; - tags?: ITag[]; -} - -export interface IEvent extends ICreateEvent { - id: number; - createdAt: Date; -} - const TABLE = 'events'; -class EventStore extends EventEmitter { +class EventStore extends EventEmitter implements IEventStore { private db: Knex; private logger: Logger; @@ -44,7 +33,7 @@ class EventStore extends EventEmitter { constructor(db: Knex, getLogger: LogProvider) { super(); this.db = db; - this.logger = getLogger('lib/db/event-store.js'); + this.logger = getLogger('lib/db/event-store.ts'); } async store(event: ICreateEvent): Promise { @@ -66,13 +55,41 @@ class EventStore extends EventEmitter { .returning(EVENT_COLUMNS); const savedEvents = savedRows.map(this.rowToEvent); process.nextTick(() => - savedEvents.forEach(e => this.emit(e.type, e)), + savedEvents.forEach((e) => this.emit(e.type, e)), ); } catch (e) { this.logger.warn('Failed to store events'); } } + async delete(key: number): Promise { + await this.db(TABLE).where({ id: key }).del(); + } + + async deleteAll(): Promise { + await this.db(TABLE).del(); + } + + destroy(): void {} + + async exists(key: number): Promise { + const result = await this.db.raw( + `SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE id = ?) AS present`, + [key], + ); + const { present } = result.rows[0]; + return present; + } + + async get(key: number): Promise { + const row = await this.db(TABLE).where({ id: key }).first(); + return this.rowToEvent(row); + } + + async getAll(): Promise { + return this.getEvents(); + } + async getEvents(): Promise { try { const rows = await this.db @@ -87,7 +104,7 @@ class EventStore extends EventEmitter { } } - async getEventsFilterByName(name: string): Promise { + async getEventsFilterByType(name: string): Promise { try { const rows = await this.db .select(EVENT_COLUMNS) @@ -116,7 +133,7 @@ class EventStore extends EventEmitter { createdBy: row.created_by, createdAt: row.created_at, data: row.data, - tags: row.tags, + tags: row.tags || [], }; } diff --git a/src/lib/db/feature-environment-store.ts b/src/lib/db/feature-environment-store.ts new file mode 100644 index 0000000000..82a2befd23 --- /dev/null +++ b/src/lib/db/feature-environment-store.ts @@ -0,0 +1,195 @@ +import EventEmitter from 'events'; +import { Knex } from 'knex'; +import { + FeatureEnvironmentKey, + IFeatureEnvironmentStore, +} from '../types/stores/feature-environment-store'; +import { Logger, LogProvider } from '../logger'; +import metricsHelper from '../util/metrics-helper'; +import { DB_TIME } from '../metric-events'; +import { IFeatureEnvironment } from '../types/model'; +import NotFoundError from '../error/notfound-error'; + +const T = { featureEnvs: 'feature_environments' }; + +export class FeatureEnvironmentStore implements IFeatureEnvironmentStore { + private db: Knex; + + private logger: Logger; + + private readonly timer: Function; + + constructor(db: Knex, eventBus: EventEmitter, getLogger: LogProvider) { + this.db = db; + this.logger = getLogger('feature-environment-store.ts'); + this.timer = (action) => + metricsHelper.wrapTimer(eventBus, DB_TIME, { + store: 'feature-environments', + action, + }); + } + + async delete({ + featureName, + environment, + }: FeatureEnvironmentKey): Promise { + await this.db(T.featureEnvs) + .where('feature_name', featureName) + .andWhere('environment', environment) + .del(); + } + + async deleteAll(): Promise { + await this.db(T.featureEnvs).del(); + } + + destroy(): void {} + + async exists({ + featureName, + environment, + }: FeatureEnvironmentKey): Promise { + const result = await this.db.raw( + `SELECT EXISTS (SELECT 1 FROM ${T.featureEnvs} WHERE feature_name = ? AND environment = ?) AS present`, + [featureName, environment], + ); + const { present } = result.rows[0]; + return present; + } + + async get({ + featureName, + environment, + }: FeatureEnvironmentKey): Promise { + const md = await this.db(T.featureEnvs) + .where('feature_name', featureName) + .andWhere('environment', environment) + .first(); + if (md) { + return { + enabled: md.enabled, + featureName, + environment, + }; + } + throw new NotFoundError( + `Could not find ${featureName} in ${environment}`, + ); + } + + async getAll(): Promise { + const rows = await this.db(T.featureEnvs); + return rows.map((r) => ({ + enabled: r.enabled, + featureName: r.feature_name, + environment: r.environment, + })); + } + + async connectEnvironmentAndFeature( + feature_name: string, + environment: string, + enabled: boolean = false, + ): Promise { + await this.db('feature_environments') + .insert({ feature_name, environment, enabled }) + .onConflict(['environment', 'feature_name']) + .merge('enabled'); + } + + async disconnectEnvironmentFromProject( + environment: string, + project: string, + ): Promise { + const featureSelector = this.db('features') + .where({ project }) + .select('name'); + await this.db(T.featureEnvs) + .where({ environment }) + .andWhere('feature_name', 'IN', featureSelector) + .del(); + await this.db('feature_strategies').where({ + environment, + project_name: project, + }); + } + + async enableEnvironmentForFeature( + feature_name: string, + environment: string, + ): Promise { + await this.db(T.featureEnvs) + .update({ enabled: true }) + .where({ feature_name, environment }); + } + + async featureHasEnvironment( + environment: string, + featureName: string, + ): Promise { + const result = await this.db.raw( + `SELECT EXISTS (SELECT 1 FROM ${T.featureEnvs} WHERE feature_name = ? AND environment = ?) AS present`, + [featureName, environment], + ); + const { present } = result.rows[0]; + return present; + } + + async getAllFeatureEnvironments(): Promise { + const rows = await this.db(T.featureEnvs); + return rows.map((r) => ({ + environment: r.environment, + featureName: r.feature_name, + enabled: r.enabled, + })); + } + + async getEnvironmentMetaData( + environment: string, + featureName: string, + ): Promise { + const md = await this.db(T.featureEnvs) + .where('feature_name', featureName) + .andWhere('environment', environment) + .first(); + if (md) { + return { + enabled: md.enabled, + featureName, + environment, + }; + } + throw new NotFoundError( + `Could not find ${featureName} in ${environment}`, + ); + } + + async isEnvironmentEnabled( + featureName: string, + environment: string, + ): Promise { + const row = await this.db(T.featureEnvs) + .select('enabled') + .where({ feature_name: featureName, environment }) + .first(); + return row.enabled; + } + + async removeEnvironmentForFeature( + feature_name: string, + environment: string, + ): Promise { + await this.db(T.featureEnvs).where({ feature_name, environment }).del(); + } + + async toggleEnvironmentEnabledStatus( + environment: string, + featureName: string, + enabled: boolean, + ): Promise { + await this.db(T.featureEnvs) + .update({ enabled }) + .where({ environment, feature_name: featureName }); + return enabled; + } +} diff --git a/src/lib/db/feature-strategy-store.ts b/src/lib/db/feature-strategy-store.ts index 55c2cf7687..a4149b6b5d 100644 --- a/src/lib/db/feature-strategy-store.ts +++ b/src/lib/db/feature-strategy-store.ts @@ -4,18 +4,16 @@ import * as uuid from 'uuid'; import metricsHelper from '../util/metrics-helper'; import { DB_TIME } from '../metric-events'; import { Logger, LogProvider } from '../logger'; +import NotFoundError from '../error/notfound-error'; import { + FeatureToggleWithEnvironment, IConstraint, - IEnvironmentOverview, - IFeatureOverview, + IFeatureStrategy, IFeatureToggleClient, IFeatureToggleQuery, IStrategyConfig, - IVariant, - FeatureToggleWithEnvironment, - IFeatureEnvironment, } from '../types/model'; -import NotFoundError from '../error/notfound-error'; +import { IFeatureStrategiesStore } from '../types/stores/feature-strategies-store'; const COLUMNS = [ 'id', @@ -52,26 +50,6 @@ interface IFeatureStrategiesTable { created_at?: Date; } -export interface IFeatureStrategy { - id: string; - featureName: string; - projectName: string; - environment: string; - strategyName: string; - parameters: object; - constraints: IConstraint[]; - createdAt?: Date; -} - -export interface FeatureConfigurationClient { - name: string; - type: string; - enabled: boolean; - stale: boolean; - strategies: IStrategyConfig[]; - variants: IVariant[]; -} - function mapRow(row: IFeatureStrategiesTable): IFeatureStrategy { return { id: row.id, @@ -80,7 +58,7 @@ function mapRow(row: IFeatureStrategiesTable): IFeatureStrategy { environment: row.environment, strategyName: row.strategy_name, parameters: row.parameters, - constraints: ((row.constraints as unknown) as IConstraint[]) || [], + constraints: (row.constraints as unknown as IConstraint[]) || [], createdAt: row.created_at, }; } @@ -118,7 +96,7 @@ function mapStrategyUpdate( return update; } -class FeatureStrategiesStore { +class FeatureStrategiesStore implements IFeatureStrategiesStore { private db: Knex; private logger: Logger; @@ -128,13 +106,39 @@ class FeatureStrategiesStore { constructor(db: Knex, eventBus: EventEmitter, getLogger: LogProvider) { this.db = db; this.logger = getLogger('feature-toggle-store.ts'); - this.timer = action => + this.timer = (action) => metricsHelper.wrapTimer(eventBus, DB_TIME, { store: 'feature-toggle-strategies', action, }); } + async delete(key: string): Promise { + await this.db(T.featureStrategies).where({ id: key }).del(); + } + + async deleteAll(): Promise { + await this.db(T.featureStrategies).delete(); + } + + destroy(): void {} + + async exists(key: string): Promise { + const result = await this.db.raw( + `SELECT EXISTS (SELECT 1 FROM ${T.featureStrategies} WHERE id = ?) AS present`, + [key], + ); + const { present } = result.rows[0]; + return present; + } + + async get(key: string): Promise { + const row = await this.db(T.featureStrategies) + .where({ id: key }) + .first(); + return mapRow(row); + } + async createStrategyConfig( strategyConfig: Omit, ): Promise { @@ -163,10 +167,6 @@ class FeatureStrategiesStore { return rows.map(mapRow); } - async deleteFeatureStrategies(): Promise { - await this.db(T.featureStrategies).delete(); - } - async getStrategiesForEnvironment( environment: string, ): Promise { @@ -394,61 +394,13 @@ class FeatureStrategiesStore { } async getStrategyById(id: string): Promise { - const strat = await this.db(T.featureStrategies) - .where({ id }) - .first(); + const strat = await this.db(T.featureStrategies).where({ id }).first(); if (strat) { return mapRow(strat); } throw new NotFoundError(`Could not find strategy with id: ${id}`); } - async connectEnvironmentAndFeature( - feature_name: string, - environment: string, - enabled: boolean = false, - ): Promise { - await this.db('feature_environments') - .insert({ feature_name, environment, enabled }) - .onConflict(['environment', 'feature_name']) - .merge('enabled'); - } - - async enableEnvironmentForFeature( - feature_name: string, - environment: string, - ): Promise { - await this.db(T.featureEnvs) - .update({ enabled: true }) - .where({ feature_name, environment }); - } - - async removeEnvironmentForFeature( - feature_name: string, - environment: string, - ): Promise { - await this.db(T.featureEnvs) - .where({ feature_name, environment }) - .del(); - } - - async disconnectEnvironmentFromProject( - environment: string, - project: string, - ): Promise { - const featureSelector = this.db('features') - .where({ project }) - .select('name'); - await this.db(T.featureEnvs) - .where({ environment }) - .andWhere('feature_name', 'IN', featureSelector) - .del(); - await this.db('feature_strategies').where({ - environment, - project_name: project, - }); - } - async updateStrategy( id: string, updates: Partial, @@ -477,26 +429,6 @@ class FeatureStrategiesStore { return strategy; } - async getEnvironmentMetaData( - environment: string, - featureName: string, - ): Promise { - const md = await this.db(T.featureEnvs) - .where('feature_name', featureName) - .andWhere('environment', environment) - .first(); - if (md) { - return { - enabled: md.enabled, - featureName, - environment, - }; - } - throw new NotFoundError( - `Could not find ${featureName} in ${environment}`, - ); - } - async getStrategiesAndMetadataForEnvironment( environment: string, featureName: string, @@ -530,58 +462,6 @@ class FeatureStrategiesStore { .where({ project_name: projectId, environment }) .del(); } - - async isEnvironmentEnabled( - featureName: string, - environment: string, - ): Promise { - const row = await this.db(T.featureEnvs) - .select('enabled') - .where({ feature_name: featureName, environment }) - .first(); - return row.enabled; - } - - async toggleEnvironmentEnabledStatus( - environment: string, - featureName: string, - enabled: boolean, - ): Promise { - await this.db(T.featureEnvs) - .update({ enabled }) - .where({ environment, feature_name: featureName }); - return enabled; - } - - async getAllFeatureEnvironments(): Promise { - const rows = await this.db(T.featureEnvs); - return rows.map(r => ({ - environment: r.environment, - featureName: r.feature_name, - enabled: r.enabled, - })); - } - - async featureHasEnvironment( - environment: string, - featureName: string, - ): Promise { - const result = await this.db.raw( - `SELECT EXISTS (SELECT 1 FROM ${T.featureEnvs} WHERE feature_name = ? AND environment = ?) AS present`, - [featureName, environment], - ); - const { present } = result.rows[0]; - return present; - } - - async hasStrategy(id: string): Promise { - const result = await this.db.raw( - `SELECT EXISTS (SELECT 1 FROM ${T.featureStrategies} WHERE id = ?) AS present`, - [id], - ); - const { present } = result.rows[0]; - return present; - } } module.exports = FeatureStrategiesStore; diff --git a/src/lib/db/feature-tag-store.ts b/src/lib/db/feature-tag-store.ts index e62a898506..2c5ddb23b4 100644 --- a/src/lib/db/feature-tag-store.ts +++ b/src/lib/db/feature-tag-store.ts @@ -6,6 +6,11 @@ import metricsHelper from '../util/metrics-helper'; import { DB_TIME } from '../metric-events'; import { UNIQUE_CONSTRAINT_VIOLATION } from '../error/db-error'; import FeatureHasTagError from '../error/feature-has-tag-error'; +import { + IFeatureAndTag, + IFeatureTag, + IFeatureTagStore, +} from '../types/stores/feature-tag-store'; const COLUMNS = ['feature_name', 'tag_type', 'tag_value']; const TABLE = 'feature_tag'; @@ -16,18 +21,7 @@ interface FeatureTagTable { tag_value: string; } -export interface IFeatureTag { - featureName: string; - tagType: string; - tagValue: string; -} - -export interface IFeatureAndTag { - featureName: string; - tag: ITag; -} - -class FeatureTagStore { +class FeatureTagStore implements IFeatureTagStore { private db: Knex; private logger: Logger; @@ -36,14 +30,71 @@ class FeatureTagStore { constructor(db: Knex, eventBus: EventEmitter, getLogger: LogProvider) { this.db = db; - this.logger = getLogger('feature-tag-store.js'); - this.timer = action => + this.logger = getLogger('feature-tag-store.ts'); + this.timer = (action) => metricsHelper.wrapTimer(eventBus, DB_TIME, { store: 'feature-toggle', action, }); } + async delete({ + featureName, + tagType, + tagValue, + }: IFeatureTag): Promise { + await this.db(TABLE) + .where({ + feature_name: featureName, + tag_type: tagType, + tag_value: tagValue, + }) + .del(); + } + + destroy(): void {} + + async exists({ + featureName, + tagType, + tagValue, + }: IFeatureTag): Promise { + const result = await this.db.raw( + `SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE feature_name = ? AND tag_type = ? AND tag_value = ?) AS present`, + [featureName, tagType, tagValue], + ); + const { present } = result.rows[0]; + return present; + } + + async get({ + featureName, + tagType, + tagValue, + }: IFeatureTag): Promise { + const row = await this.db(TABLE) + .where({ + feature_name: featureName, + tag_type: tagType, + tag_value: tagValue, + }) + .first(); + return { + featureName: row.feature_name, + tagType: row.tag_type, + tagValue: row.tag_value, + }; + } + + async getAll(): Promise { + const rows = await this.db(TABLE).select(COLUMNS); + return rows.map((row) => ({ + featureName: row.feature_name, + tagType: row.tag_type, + tagValue: row.tag_value, + })); + } + async getAllTagsForFeature(featureName: string): Promise { const stopTimer = this.timer('getAllForFeature'); const rows = await this.db @@ -58,7 +109,7 @@ class FeatureTagStore { const stopTimer = this.timer('tagFeature'); await this.db(TABLE) .insert(this.featureAndTagToRow(featureName, tag)) - .catch(err => { + .catch((err) => { if (err.code === UNIQUE_CONSTRAINT_VIOLATION) { throw new FeatureHasTagError( `${featureName} already had the tag: [${tag.type}:${tag.value}]`, @@ -79,19 +130,17 @@ class FeatureTagStore { .select(COLUMNS) .whereIn( 'feature_name', - this.db('features') - .where({ archived: false }) - .select(['name']), + this.db('features').where({ archived: false }).select(['name']), ); - return rows.map(row => ({ + return rows.map((row) => ({ featureName: row.feature_name, tagType: row.tag_type, tagValue: row.tag_value, })); } - async dropFeatureTags(): Promise { - const stopTimer = this.timer('dropFeatureTags'); + async deleteAll(): Promise { + const stopTimer = this.timer('deleteAll'); await this.db(TABLE).del(); stopTimer(); } diff --git a/src/lib/db/feature-toggle-store.ts b/src/lib/db/feature-toggle-store.ts index 0d5bc3232a..6d21397d7c 100644 --- a/src/lib/db/feature-toggle-store.ts +++ b/src/lib/db/feature-toggle-store.ts @@ -5,6 +5,7 @@ import { DB_TIME } from '../metric-events'; import NotFoundError from '../error/notfound-error'; import { Logger, LogProvider } from '../logger'; import { FeatureToggleDTO, FeatureToggle, IVariant } from '../types/model'; +import { IFeatureToggleStore } from '../types/stores/feature-toggle-store'; const FEATURE_COLUMNS = [ 'name', @@ -30,7 +31,7 @@ export interface FeaturesTable { const TABLE = 'features'; -export default class FeatureToggleStore { +export default class FeatureToggleStore implements IFeatureToggleStore { private db: Knex; private logger: Logger; @@ -40,7 +41,7 @@ export default class FeatureToggleStore { constructor(db: Knex, eventBus: EventEmitter, getLogger: LogProvider) { this.db = db; this.logger = getLogger('feature-toggle-store.ts'); - this.timer = action => + this.timer = (action) => metricsHelper.wrapTimer(eventBus, DB_TIME, { store: 'feature-toggle', action, @@ -58,7 +59,7 @@ export default class FeatureToggleStore { .count('*') .from(TABLE) .where(query) - .then(res => Number(res[0].count)); + .then((res) => Number(res[0].count)); } async getFeatureMetadata(name: string): Promise { @@ -69,7 +70,29 @@ export default class FeatureToggleStore { .then(this.rowToFeature); } - async getFeatures(archived: boolean = false): Promise { + async deleteAll(): Promise { + await this.db(TABLE).del(); + } + + destroy(): void {} + + async get(name: string): Promise { + return this.db + .first(FEATURE_COLUMNS) + .from(TABLE) + .where({ name, archived: 0 }) + .then(this.rowToFeature); + } + + async getAll(): Promise { + const rows = await this.db + .select(FEATURE_COLUMNS) + .from(TABLE) + .where({ archived: false }); + return rows.map(this.rowToFeature); + } + + async getFeatures(archived: boolean): Promise { const rows = await this.db .select(FEATURE_COLUMNS) .from(TABLE) @@ -87,8 +110,8 @@ export default class FeatureToggleStore { .first(['project']) .from(TABLE) .where({ name }) - .then(r => (r ? r.project : undefined)) - .catch(e => { + .then((r) => (r ? r.project : undefined)) + .catch((e) => { this.logger.error(e); return undefined; }); @@ -103,7 +126,7 @@ export default class FeatureToggleStore { .first('name', 'archived') .from(TABLE) .where({ name }) - .then(row => { + .then((row) => { if (!row) { throw new NotFoundError('No feature toggle found'); } @@ -116,7 +139,7 @@ export default class FeatureToggleStore { async exists(name: string): Promise { const result = await this.db.raw( - `SELECT EXISTS (SELECT 1 FROM features WHERE name = ?) AS present`, + 'SELECT EXISTS (SELECT 1 FROM features WHERE name = ?) AS present', [name], ); const { present } = result.rows[0]; @@ -132,7 +155,7 @@ export default class FeatureToggleStore { return rows.map(this.rowToFeature); } - async lastSeenToggles(toggleNames: string[]): Promise { + async updateLastSeenForToggles(toggleNames: string[]): Promise { const now = new Date(); try { await this.db(TABLE) @@ -160,7 +183,7 @@ export default class FeatureToggleStore { type: row.type, project: row.project, stale: row.stale, - variants: (row.variants as unknown) as IVariant[], + variants: row.variants as unknown as IVariant[], createdAt: row.created_at, lastSeenAt: row.last_seen_at, }; @@ -218,7 +241,7 @@ export default class FeatureToggleStore { return this.rowToFeature(row[0]); } - async deleteFeature(name: string): Promise { + async delete(name: string): Promise { await this.db(TABLE) .where({ name, archived: true }) // Feature toggle must be archived to allow deletion .del(); @@ -232,14 +255,6 @@ export default class FeatureToggleStore { return this.rowToFeature(row[0]); } - async dropFeatures(): Promise { - try { - await this.db(TABLE).delete(); - } catch (err) { - this.logger.error('Could not drop features, error: ', err); - } - } - async getFeaturesBy(params: { archived?: boolean; project?: string; diff --git a/src/lib/db/feature-type-store.ts b/src/lib/db/feature-type-store.ts index 7637fd245d..b7c8119985 100644 --- a/src/lib/db/feature-type-store.ts +++ b/src/lib/db/feature-type-store.ts @@ -1,16 +1,13 @@ import { Knex } from 'knex'; import { Logger, LogProvider } from '../logger'; +import { + IFeatureType, + IFeatureTypeStore, +} from '../types/stores/feature-type-store'; const COLUMNS = ['id', 'name', 'description', 'lifetime_days']; const TABLE = 'feature_types'; -export interface IFeatureType { - id: number; - name: string; - description: string; - lifetimeDays: number; -} - interface IFeatureTypeRow { id: number; name: string; @@ -18,14 +15,14 @@ interface IFeatureTypeRow { lifetime_days: number; } -class FeatureTypeStore { +class FeatureTypeStore implements IFeatureTypeStore { private db: Knex; private logger: Logger; constructor(db: Knex, getLogger: LogProvider) { this.db = db; - this.logger = getLogger('feature-type-store.js'); + this.logger = getLogger('feature-type-store.ts'); } async getAll(): Promise { @@ -33,7 +30,7 @@ class FeatureTypeStore { return rows.map(this.rowToFeatureType); } - rowToFeatureType(row: IFeatureTypeRow): IFeatureType { + private rowToFeatureType(row: IFeatureTypeRow): IFeatureType { return { id: row.id, name: row.name, @@ -41,6 +38,35 @@ class FeatureTypeStore { lifetimeDays: row.lifetime_days, }; } + + async get(id: number): Promise { + const row = await this.db(TABLE).where({ id }).first(); + return this.rowToFeatureType(row); + } + + async getByName(name: string): Promise { + const row = await this.db(TABLE).where({ name }).first(); + return this.rowToFeatureType(row); + } + + async delete(key: number): Promise { + await this.db(TABLE).where({ id: key }).del(); + } + + async deleteAll(): Promise { + await this.db(TABLE).del(); + } + + destroy(): void {} + + async exists(key: number): Promise { + const result = await this.db.raw( + `SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE id = ?) AS present`, + [key], + ); + const { present } = result.rows[0]; + return present; + } } export default FeatureTypeStore; module.exports = FeatureTypeStore; diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts index 98b5c017c4..d0f5b5f796 100644 --- a/src/lib/db/index.ts +++ b/src/lib/db/index.ts @@ -1,9 +1,8 @@ -// eslint-disable-next-line import EventEmitter from 'events'; +import { Knex } from 'knex'; import { IUnleashConfig } from '../types/option'; import { IUnleashStores } from '../types/stores'; -import { createDb } from './db-pool'; import EventStore from './event-store'; import FeatureToggleStore from './feature-toggle-store'; import FeatureTypeStore from './feature-type-store'; @@ -27,23 +26,27 @@ import UserFeedbackStore from './user-feedback-store'; import FeatureStrategyStore from './feature-strategy-store'; import EnvironmentStore from './environment-store'; import FeatureTagStore from './feature-tag-store'; +import { FeatureEnvironmentStore } from './feature-environment-store'; export const createStores = ( config: IUnleashConfig, eventBus: EventEmitter, + db: Knex, ): IUnleashStores => { const { getLogger } = config; - const db = createDb(config); const eventStore = new EventStore(db, getLogger); const clientMetricsDb = new ClientMetricsDb(db, getLogger); return { - db, eventStore, featureToggleStore: new FeatureToggleStore(db, eventBus, getLogger), featureTypeStore: new FeatureTypeStore(db, getLogger), strategyStore: new StrategyStore(db, getLogger), - clientApplicationsStore: new ClientApplicationsStore(db, eventBus), + clientApplicationsStore: new ClientApplicationsStore( + db, + eventBus, + getLogger, + ), clientInstanceStore: new ClientInstanceStore(db, eventBus, getLogger), clientMetricsStore: new ClientMetricsStore( clientMetricsDb, @@ -69,6 +72,11 @@ export const createStores = ( ), environmentStore: new EnvironmentStore(db, eventBus, getLogger), featureTagStore: new FeatureTagStore(db, eventBus, getLogger), + featureEnvironmentStore: new FeatureEnvironmentStore( + db, + eventBus, + getLogger, + ), }; }; diff --git a/src/lib/db/project-store.ts b/src/lib/db/project-store.ts index e2d16c0a21..33a77a5793 100644 --- a/src/lib/db/project-store.ts +++ b/src/lib/db/project-store.ts @@ -3,44 +3,27 @@ import { Logger, LogProvider } from '../logger'; import NotFoundError from '../error/notfound-error'; import { IEnvironmentOverview, IFeatureOverview } from '../types/model'; +import { + IProject, + IProjectHealthUpdate, + IProjectInsert, + IProjectStore, +} from '../types/stores/project-store'; const COLUMNS = ['id', 'name', 'description', 'created_at', 'health']; const TABLE = 'projects'; -export interface IProject { - id: string; - name: string; - description: string; - health: number; - createdAt: Date; -} - -interface IProjectInsert { - id: string; - name: string; - description: string; -} - -interface IProjectArchived { - id: string; - archived: boolean; -} - -interface IProjectHealthUpdate { - id: string; - health: number; -} - -class ProjectStore { +class ProjectStore implements IProjectStore { private db: Knex; private logger: Logger; constructor(db: Knex, getLogger: LogProvider) { this.db = db; - this.logger = getLogger('project-store.js'); + this.logger = getLogger('project-store.ts'); } + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types fieldToRow(data): IProjectInsert { return { id: data.id, @@ -49,6 +32,17 @@ class ProjectStore { }; } + destroy(): void {} + + async exists(id: string): Promise { + const result = await this.db.raw( + `SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE id = ?) AS present`, + [id], + ); + const { present } = result.rows[0]; + return present; + } + async getAll(): Promise { const rows = await this.db .select(COLUMNS) @@ -66,20 +60,13 @@ class ProjectStore { .then(this.mapRow); } - async hasProject(id: string): Promise { - return this.db - .first('id') - .from(TABLE) - .where({ id }) - .then(row => { - if (!row) { - throw new NotFoundError(`No project with id=${id} found`); - } - return { - id: row.id, - archived: row.archived === 1, - }; - }); + async hasProject(id: string): Promise { + const result = await this.db.raw( + `SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE id = ?) AS present`, + [id], + ); + const { present } = result.rows[0]; + return present; } async updateHealth(healthUpdate: IProjectHealthUpdate): Promise { @@ -88,13 +75,14 @@ class ProjectStore { .update({ health: healthUpdate.health }); } - async create(project): Promise { - const [id] = await this.db(TABLE) + async create(project: IProjectInsert): Promise { + const row = await this.db(TABLE) .insert(this.fieldToRow(project)) - .returning('id'); - return { ...project, id }; + .returning('*'); + return this.mapRow(row); } + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async update(data): Promise { try { await this.db(TABLE) @@ -105,7 +93,7 @@ class ProjectStore { } } - async importProjects(projects): Promise { + async importProjects(projects: IProjectInsert[]): Promise { const rows = await this.db(TABLE) .insert(projects.map(this.fieldToRow)) .returning(COLUMNS) @@ -118,8 +106,8 @@ class ProjectStore { return []; } - async addGlobalEnvironment(projects): Promise { - const environments = projects.map(p => ({ + async addGlobalEnvironment(projects: any[]): Promise { + const environments = projects.map((p) => ({ project_id: p.id, environment_name: ':global:', })); @@ -129,21 +117,22 @@ class ProjectStore { .ignore(); } - async dropProjects(): Promise { + async deleteAll(): Promise { await this.db(TABLE).del(); } async delete(id: string): Promise { try { - await this.db(TABLE) - .where({ id }) - .del(); + await this.db(TABLE).where({ id }).del(); } catch (err) { this.logger.error('Could not delete project, error: ', err); } } - async deleteEnvironment(id: string, environment: string): Promise { + async deleteEnvironmentForProject( + id: string, + environment: string, + ): Promise { await this.db('project_environments') .where({ project_id: id, @@ -152,6 +141,24 @@ class ProjectStore { .del(); } + async addEnvironmentToProject( + id: string, + environment: string, + ): Promise { + await this.db('project_environments') + .insert({ project_id: id, environment_name: environment }) + .onConflict(['project_id', 'environment_name']) + .ignore(); + } + + async getEnvironmentsForProject(id: string): Promise { + return this.db('project_environments') + .where({ + project_id: id, + }) + .returning('environment_name'); + } + async getMembers(projectId: string): Promise { const rolesFromProject = this.db('role_permission') .select('role_id') @@ -215,7 +222,7 @@ class ProjectStore { }, {}); return Object.values(overview).map((o: IFeatureOverview) => ({ ...o, - environments: o.environments.filter(f => f.name), + environments: o.environments.filter((f) => f.name), })); } return []; @@ -230,6 +237,7 @@ class ProjectStore { }; } + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types mapRow(row): IProject { if (!row) { throw new NotFoundError('No project found'); diff --git a/src/lib/db/reset-token-store.ts b/src/lib/db/reset-token-store.ts index 1230e70df7..72daa8639a 100644 --- a/src/lib/db/reset-token-store.ts +++ b/src/lib/db/reset-token-store.ts @@ -4,6 +4,13 @@ import metricsHelper from '../util/metrics-helper'; import { DB_TIME } from '../metric-events'; import { Logger, LogProvider } from '../logger'; import NotFoundError from '../error/notfound-error'; +import { + IResetQuery, + IResetToken, + IResetTokenCreate, + IResetTokenQuery, + IResetTokenStore, +} from '../types/stores/reset-token-store'; const TABLE = 'reset_tokens'; @@ -16,44 +23,16 @@ interface IResetTokenTable { used_at: Date; } -export interface IResetTokenCreate { - reset_token: string; - user_id: number; - expires_at: Date; - created_by?: string; -} +const rowToResetToken = (row: IResetTokenTable): IResetToken => ({ + userId: row.user_id, + token: row.reset_token, + expiresAt: row.expires_at, + createdAt: row.created_at, + createdBy: row.created_by, + usedAt: row.used_at, +}); -export interface IResetToken { - userId: number; - token: string; - createdBy: string; - expiresAt: Date; - createdAt: Date; - usedAt?: Date; -} - -export interface IResetQuery { - userId: number; - token: string; -} - -export interface IResetTokenQuery { - user_id: number; - reset_token: string; -} - -const rowToResetToken = (row: IResetTokenTable): IResetToken => { - return { - userId: row.user_id, - token: row.reset_token, - expiresAt: row.expires_at, - createdAt: row.created_at, - createdBy: row.created_by, - usedAt: row.used_at, - }; -}; - -export class ResetTokenStore { +export class ResetTokenStore implements IResetTokenStore { private logger: Logger; private timer: Function; @@ -62,7 +41,7 @@ export class ResetTokenStore { constructor(db: Knex, eventBus: EventEmitter, getLogger: LogProvider) { this.db = db; - this.logger = getLogger('db/reset-token-store.js'); + this.logger = getLogger('db/reset-token-store.ts'); this.timer = (action: string) => metricsHelper.wrapTimer(eventBus, DB_TIME, { store: 'reset-tokens', @@ -113,10 +92,8 @@ export class ResetTokenStore { } } - async delete({ reset_token }: IResetTokenQuery): Promise { - return this.db(TABLE) - .where(reset_token) - .del(); + async deleteFromQuery({ reset_token }: IResetTokenQuery): Promise { + return this.db(TABLE).where(reset_token).del(); } async deleteAll(): Promise { @@ -124,16 +101,37 @@ export class ResetTokenStore { } async deleteExpired(): Promise { - return this.db(TABLE) - .where('expires_at', '<', new Date()) - .del(); + return this.db(TABLE).where('expires_at', '<', new Date()).del(); } async expireExistingTokensForUser(user_id: number): Promise { - await this.db(TABLE) - .where({ user_id }) - .update({ - expires_at: new Date(), - }); + await this.db(TABLE).where({ user_id }).update({ + expires_at: new Date(), + }); + } + + async delete(reset_token: string): Promise { + await this.db(TABLE).where({ reset_token }).del(); + } + + destroy(): void {} + + async exists(reset_token: string): Promise { + const result = await this.db.raw( + `SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE reset_token = ?) AS present`, + [reset_token], + ); + const { present } = result.rows[0]; + return present; + } + + async get(key: string): Promise { + const row = await this.db(TABLE).where({ reset_token: key }).first(); + return rowToResetToken(row); + } + + async getAll(): Promise { + const rows = await this.db(TABLE).select(); + return rows.map(rowToResetToken); } } diff --git a/src/lib/db/session-store.ts b/src/lib/db/session-store.ts index e340a10c86..d8bddf8cbd 100644 --- a/src/lib/db/session-store.ts +++ b/src/lib/db/session-store.ts @@ -2,6 +2,7 @@ import EventEmitter from 'events'; import { Knex } from 'knex'; import { Logger, LogProvider } from '../logger'; import NotFoundError from '../error/notfound-error'; +import { ISession, ISessionStore } from '../types/stores/session-store'; const TABLE = 'unleash_session'; @@ -12,14 +13,7 @@ interface ISessionRow { expired?: Date; } -export interface ISession { - sid: string; - sess: any; - createdAt: Date; - expired?: Date; -} - -export default class SessionStore { +export default class SessionStore implements ISessionStore { private logger: Logger; private eventBus: EventEmitter; @@ -41,9 +35,10 @@ export default class SessionStore { } async getSessionsForUser(userId: number): Promise { - const rows = await this.db( - TABLE, - ).whereRaw(`(sess -> 'user' ->> 'id')::int = ?`, [userId]); + const rows = await this.db(TABLE).whereRaw( + "(sess -> 'user' ->> 'id')::int = ?", + [userId], + ); if (rows && rows.length > 0) { return rows.map(this.rowToSession); } @@ -52,7 +47,7 @@ export default class SessionStore { ); } - async getSession(sid: string): Promise { + async get(sid: string): Promise { const row = await this.db(TABLE) .where('sid', '=', sid) .first(); @@ -64,14 +59,12 @@ export default class SessionStore { async deleteSessionsForUser(userId: number): Promise { await this.db(TABLE) - .whereRaw(`(sess -> 'user' ->> 'id')::int = ?`, [userId]) + .whereRaw("(sess -> 'user' ->> 'id')::int = ?", [userId]) .del(); } - async deleteSession(sid: string): Promise { - await this.db(TABLE) - .where('sid', '=', sid) - .del(); + async delete(sid: string): Promise { + await this.db(TABLE).where('sid', '=', sid).del(); } async insertSession(data: Omit): Promise { @@ -92,6 +85,22 @@ export default class SessionStore { await this.db(TABLE).del(); } + destroy(): void {} + + async exists(sid: string): Promise { + const result = await this.db.raw( + `SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE sid = ?) AS present`, + [sid], + ); + const { present } = result.rows[0]; + return present; + } + + async getAll(): Promise { + const rows = await this.db(TABLE); + return rows.map(this.rowToSession); + } + private rowToSession(row: ISessionRow): ISession { return { sid: row.sid, diff --git a/src/lib/db/setting-store.js b/src/lib/db/setting-store.js deleted file mode 100644 index 9737793b5d..0000000000 --- a/src/lib/db/setting-store.js +++ /dev/null @@ -1,50 +0,0 @@ -/* eslint camelcase: "off" */ - -'use strict'; - -const TABLE = 'settings'; - -class SettingStore { - constructor(db, getLogger) { - this.db = db; - this.logger = getLogger('settings-store.js'); - } - - async updateRow(name, content) { - return this.db(TABLE) - .where('name', name) - .update({ - content: JSON.stringify(content), - }); - } - - async insertNewRow(name, content) { - return this.db(TABLE).insert({ name, content }); - } - - async insert(name, content) { - const result = await this.db(TABLE) - .count('*') - .where('name', name) - .first(); - - if (Number(result.count) > 0) { - return this.updateRow(name, content); - } - return this.insertNewRow(name, content); - } - - async get(name) { - const result = await this.db - .select() - .from(TABLE) - .where('name', name); - - if (result.length > 0) { - return result[0].content; - } - return undefined; - } -} - -module.exports = SettingStore; diff --git a/src/lib/db/setting-store.ts b/src/lib/db/setting-store.ts new file mode 100644 index 0000000000..f42ef1add7 --- /dev/null +++ b/src/lib/db/setting-store.ts @@ -0,0 +1,75 @@ +import { Knex } from 'knex'; +import { Logger, LogProvider } from '../logger'; +import { ISettingStore } from '../types/stores/settings-store'; + +const TABLE = 'settings'; + +export default class SettingStore implements ISettingStore { + private db: Knex; + + private logger: Logger; + + constructor(db: Knex, getLogger: LogProvider) { + this.db = db; + this.logger = getLogger('settings-store.ts'); + } + + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + async updateRow(name: string, content: any): Promise { + return this.db(TABLE) + .where('name', name) + .update({ + content: JSON.stringify(content), + }); + } + + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + async insertNewRow(name: string, content: any) { + return this.db(TABLE).insert({ name, content }); + } + + async exists(name: string): Promise { + const result = await this.db.raw( + `SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE name = ?) AS present`, + [name], + ); + const { present } = result.rows[0]; + return present; + } + + async get(name: string): Promise { + const result = await this.db.select().from(TABLE).where('name', name); + + if (result.length > 0) { + return result[0].content; + } + return undefined; + } + + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + async insert(name: string, content: any): Promise { + const exists = await this.exists(name); + if (exists) { + await this.updateRow(name, content); + } else { + await this.insertNewRow(name, content); + } + } + + async delete(name: string): Promise { + await this.db(TABLE).where({ name }).del(); + } + + async deleteAll(): Promise { + await this.db(TABLE).del(); + } + + destroy(): void {} + + async getAll(): Promise { + const rows = await this.db(TABLE).select(); + return rows.map((r) => r.content); + } +} + +module.exports = SettingStore; diff --git a/src/lib/db/strategy-store.ts b/src/lib/db/strategy-store.ts index 25a958e05c..2af9bf4f96 100644 --- a/src/lib/db/strategy-store.ts +++ b/src/lib/db/strategy-store.ts @@ -2,6 +2,12 @@ import { Knex } from 'knex'; import { Logger, LogProvider } from '../logger'; import NotFoundError from '../error/notfound-error'; +import { + IEditableStrategy, + IMinimalStrategyRow, + IStrategy, + IStrategyStore, +} from '../types/stores/strategy-store'; const STRATEGY_COLUMNS = [ 'name', @@ -13,47 +19,25 @@ const STRATEGY_COLUMNS = [ ]; const TABLE = 'strategies'; -export interface IStrategy { - name: string; - editable: boolean; - description: string; - parameters: object; - deprecated: boolean; - displayName: string; -} - -export interface IEditableStrategy { - name: string; - description: string; - parameters: object; - deprecated: boolean; -} - -export interface IMinimalStrategy { - name: string; - description: string; - parameters: string; -} - interface IStrategyRow { name: string; built_in: number; description: string; - parameters: object; + parameters: object[]; deprecated: boolean; display_name: string; } -export default class StrategyStore { +export default class StrategyStore implements IStrategyStore { private db: Knex; private logger: Logger; constructor(db: Knex, getLogger: LogProvider) { this.db = db; - this.logger = getLogger('strategy-store.js'); + this.logger = getLogger('strategy-store.ts'); } - async getStrategies(): Promise { + async getAll(): Promise { const rows = await this.db .select(STRATEGY_COLUMNS) .from(TABLE) @@ -82,6 +66,30 @@ export default class StrategyStore { .then(this.rowToStrategy); } + async delete(name: string): Promise { + await this.db(TABLE).where({ name }).del(); + } + + async deleteAll(): Promise { + await this.db(TABLE).del(); + } + + destroy(): void {} + + async exists(name: string): Promise { + const result = await this.db.raw( + `SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE name = ?) AS present`, + [name], + ); + const { present } = result.rows[0]; + return present; + } + + async get(name: string): Promise { + const row = await this.db(TABLE).where({ name }).first(); + return this.rowToStrategy(row); + } + rowToStrategy(row: IStrategyRow): IStrategy { if (!row) { throw new NotFoundError('No strategy found'); @@ -109,7 +117,7 @@ export default class StrategyStore { } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - eventDataToRow(data): IMinimalStrategy { + eventDataToRow(data): IMinimalStrategyRow { return { name: data.name, description: data.description, @@ -119,67 +127,39 @@ export default class StrategyStore { // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async createStrategy(data): Promise { - this.db(TABLE) - .insert(this.eventDataToRow(data)) - .catch(err => - this.logger.error('Could not insert strategy, error: ', err), - ); + await this.db(TABLE).insert(this.eventDataToRow(data)); } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async updateStrategy(data): Promise { - this.db(TABLE) + await this.db(TABLE) .where({ name: data.name }) - .update(this.eventDataToRow(data)) - .catch(err => - this.logger.error('Could not update strategy, error: ', err), - ); + .update(this.eventDataToRow(data)); } async deprecateStrategy({ name }: Pick): Promise { - this.db(TABLE) - .where({ name }) - .update({ deprecated: true }) - .catch(err => - this.logger.error('Could not deprecate strategy, error: ', err), - ); + await this.db(TABLE).where({ name }).update({ deprecated: true }); } async reactivateStrategy({ name }: Pick): Promise { - this.db(TABLE) - .where({ name }) - .update({ deprecated: false }) - .catch(err => - this.logger.error( - 'Could not reactivate strategy, error: ', - err, - ), - ); + await this.db(TABLE).where({ name }).update({ deprecated: false }); } async deleteStrategy({ name }: Pick): Promise { - await this.db(TABLE) - .where({ name }) - .del() - .catch(err => { - this.logger.error('Could not delete strategy, error: ', err); - }); + await this.db(TABLE).where({ name }).del(); } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async importStrategy(data): Promise { const rowData = this.eventDataToRow(data); - await this.db(TABLE) - .insert(rowData) - .onConflict(['name']) - .merge(); + await this.db(TABLE).insert(rowData).onConflict(['name']).merge(); } async dropStrategies(): Promise { await this.db(TABLE) .where({ built_in: 0 }) // eslint-disable-line .delete() - .catch(err => + .catch((err) => this.logger.error('Could not drop strategies, error: ', err), ); } diff --git a/src/lib/db/tag-store.ts b/src/lib/db/tag-store.ts index 6ddd6d47b0..39228fab73 100644 --- a/src/lib/db/tag-store.ts +++ b/src/lib/db/tag-store.ts @@ -4,6 +4,8 @@ import { DB_TIME } from '../metric-events'; import metricsHelper from '../util/metrics-helper'; import { LogProvider, Logger } from '../logger'; import NotFoundError from '../error/notfound-error'; +import { ITag } from '../types/model'; +import { ITagStore } from '../types/stores/tag-store'; const COLUMNS = ['type', 'value']; const TABLE = 'tags'; @@ -13,12 +15,7 @@ interface ITagTable { value: string; } -export interface ITag { - type: string; - value: string; -} - -export default class TagStore { +export default class TagStore implements ITagStore { private db: Knex; private logger: Logger; @@ -27,8 +24,8 @@ export default class TagStore { constructor(db: Knex, eventBus: EventEmitter, getLogger: LogProvider) { this.db = db; - this.logger = getLogger('tag-store.js'); - this.timer = action => + this.logger = getLogger('tag-store.ts'); + this.timer = (action) => metricsHelper.wrapTimer(eventBus, DB_TIME, { store: 'tag', action, @@ -37,10 +34,7 @@ export default class TagStore { async getTagsByType(type: string): Promise { const stopTimer = this.timer('getTagByType'); - const rows = await this.db - .select(COLUMNS) - .from(TABLE) - .where({ type }); + const rows = await this.db.select(COLUMNS).from(TABLE).where({ type }); stopTimer(); return rows.map(this.rowToTag); } @@ -84,16 +78,14 @@ export default class TagStore { stopTimer(); } - async deleteTag(tag: ITag): Promise { + async delete(tag: ITag): Promise { const stopTimer = this.timer('deleteTag'); - await this.db(TABLE) - .where(tag) - .del(); + await this.db(TABLE).where(tag).del(); stopTimer(); } - async dropTags(): Promise { - const stopTimer = this.timer('dropTags'); + async deleteAll(): Promise { + const stopTimer = this.timer('deleteAll'); await this.db(TABLE).del(); stopTimer(); } @@ -106,6 +98,23 @@ export default class TagStore { .ignore(); } + destroy(): void {} + + async get({ type, value }: ITag): Promise { + const stopTimer = this.timer('getTag'); + const tag = await this.db + .first(COLUMNS) + .from(TABLE) + .where({ type, value }); + stopTimer(); + if (!tag) { + throw new NotFoundError( + `No tag with type: [${type}] and value [${value}]`, + ); + } + return tag; + } + rowToTag(row: ITagTable): ITag { return { type: row.type, diff --git a/src/lib/db/tag-type-store.ts b/src/lib/db/tag-type-store.ts index 9e7cf9fba1..b19c72c80e 100644 --- a/src/lib/db/tag-type-store.ts +++ b/src/lib/db/tag-type-store.ts @@ -4,6 +4,7 @@ import { LogProvider, Logger } from '../logger'; import { DB_TIME } from '../metric-events'; import metricsHelper from '../util/metrics-helper'; import NotFoundError from '../error/notfound-error'; +import { ITagType, ITagTypeStore } from '../types/stores/tag-type-store'; const COLUMNS = ['name', 'description', 'icon']; const TABLE = 'tag_types'; @@ -14,13 +15,7 @@ interface ITagTypeTable { icon?: string; } -export interface ITagType { - name: string; - description?: string; - icon?: string; -} - -export default class TagTypeStore { +export default class TagTypeStore implements ITagTypeStore { private db: Knex; private logger: Logger; @@ -29,8 +24,8 @@ export default class TagTypeStore { constructor(db: Knex, eventBus: EventEmitter, getLogger: LogProvider) { this.db = db; - this.logger = getLogger('tag-type-store.js'); - this.timer = action => + this.logger = getLogger('tag-type-store.ts'); + this.timer = (action) => metricsHelper.wrapTimer(eventBus, DB_TIME, { store: 'tag-type', action, @@ -44,13 +39,13 @@ export default class TagTypeStore { return rows.map(this.rowToTagType); } - async getTagType(name: string): Promise { + async get(name: string): Promise { const stopTimer = this.timer('getTagTypeByName'); return this.db .first(COLUMNS) .from(TABLE) .where({ name }) - .then(row => { + .then((row) => { stopTimer(); if (!row) { throw new NotFoundError('Could not find tag-type'); @@ -60,7 +55,7 @@ export default class TagTypeStore { }); } - async exists(name: string): Promise { + async exists(name: string): Promise { const stopTimer = this.timer('exists'); const result = await this.db.raw( `SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE name = ?) AS present`, @@ -77,16 +72,14 @@ export default class TagTypeStore { stopTimer(); } - async deleteTagType(name: string): Promise { + async delete(name: string): Promise { const stopTimer = this.timer('deleteTagType'); - await this.db(TABLE) - .where({ name }) - .del(); + await this.db(TABLE).where({ name }).del(); stopTimer(); } - async dropTagTypes(): Promise { - const stopTimer = this.timer('dropTagTypes'); + async deleteAll(): Promise { + const stopTimer = this.timer('deleteAll'); await this.db(TABLE).del(); stopTimer(); } @@ -105,12 +98,12 @@ export default class TagTypeStore { async updateTagType({ name, description, icon }: ITagType): Promise { const stopTimer = this.timer('updateTagType'); - await this.db(TABLE) - .where({ name }) - .update({ description, icon }); + await this.db(TABLE).where({ name }).update({ description, icon }); stopTimer(); } + destroy(): void {} + rowToTagType(row: ITagTypeTable): ITagType { return { name: row.name, diff --git a/src/lib/db/user-feedback-store.ts b/src/lib/db/user-feedback-store.ts index fe1d2b6c86..126d4a5fb0 100644 --- a/src/lib/db/user-feedback-store.ts +++ b/src/lib/db/user-feedback-store.ts @@ -1,6 +1,11 @@ import { Knex } from 'knex'; import { EventEmitter } from 'events'; import { LogProvider, Logger } from '../logger'; +import { + IUserFeedback, + IUserFeedbackKey, + IUserFeedbackStore, +} from '../types/stores/user-feedback-store'; const COLUMNS = ['given', 'user_id', 'feedback_id', 'nevershow']; const TABLE = 'user_feedback'; @@ -12,13 +17,6 @@ interface IUserFeedbackTable { user_id: number; } -export interface IUserFeedback { - neverShow: boolean; - feedbackId: string; - given?: Date; - userId: number; -} - const fieldToRow = (fields: IUserFeedback): IUserFeedbackTable => ({ nevershow: fields.neverShow, feedback_id: fields.feedbackId, @@ -33,14 +31,14 @@ const rowToField = (row: IUserFeedbackTable): IUserFeedback => ({ userId: row.user_id, }); -export default class UserFeedbackStore { +export default class UserFeedbackStore implements IUserFeedbackStore { private db: Knex; private logger: Logger; constructor(db: Knex, eventBus: EventEmitter, getLogger: LogProvider) { this.db = db; - this.logger = getLogger('user-feedback-store.js'); + this.logger = getLogger('user-feedback-store.ts'); } async getAllUserFeedback(userId: number): Promise { @@ -75,6 +73,42 @@ export default class UserFeedbackStore { return rowToField(insertedFeedback[0]); } + + async delete({ userId, feedbackId }: IUserFeedbackKey): Promise { + await this.db(TABLE) + .where({ user_id: userId, feedback_id: feedbackId }) + .del(); + } + + async deleteAll(): Promise { + await this.db(TABLE).del(); + } + + destroy(): void {} + + async exists({ userId, feedbackId }: IUserFeedbackKey): Promise { + const result = await this.db.raw( + `SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE user_id = ? AND feedback_id = ?) AS present`, + [userId, feedbackId], + ); + const { present } = result.rows[0]; + return present; + } + + async get({ + userId, + feedbackId, + }: IUserFeedbackKey): Promise { + return this.getFeedback(userId, feedbackId); + } + + async getAll(): Promise { + const userFeedbacks = await this.db + .table(TABLE) + .select(); + + return userFeedbacks.map(rowToField); + } } module.exports = UserFeedbackStore; diff --git a/src/lib/db/user-store.ts b/src/lib/db/user-store.ts index d361e92234..dff6ef35ae 100644 --- a/src/lib/db/user-store.ts +++ b/src/lib/db/user-store.ts @@ -5,6 +5,13 @@ import { Logger, LogProvider } from '../logger'; import User from '../types/user'; import NotFoundError from '../error/notfound-error'; +import { + ICreateUser, + IUserLookup, + IUserSearch, + IUserStore, + IUserUpdateFields, +} from '../types/stores/user-store'; const TABLE = 'users'; @@ -21,7 +28,7 @@ const USER_COLUMNS = [ const USER_COLUMNS_PUBLIC = ['id', 'name', 'username', 'email', 'image_url']; -const emptify = value => { +const emptify = (value) => { if (!value) { return undefined; } @@ -37,14 +44,7 @@ const mapUserToColumns = (user: ICreateUser) => ({ image_url: user.imageUrl, }); -interface ICreateUser { - name?: string; - username?: string; - email?: string; - imageUrl?: string; -} - -const rowToUser = row => { +const rowToUser = (row) => { if (!row) { throw new NotFoundError('No user found'); } @@ -60,38 +60,19 @@ const rowToUser = row => { }); }; -export interface IUserLookup { - id?: number; - username?: string; - email?: string; -} - -export interface IUserSearch { - name?: string; - username?: string; - email: string; -} - -export interface IUserUpdateFields { - name?: string; - email?: string; -} - -class UserStore { +class UserStore implements IUserStore { private db: Knex; private logger: Logger; constructor(db: Knex, getLogger: LogProvider) { this.db = db; - this.logger = getLogger('user-store.js'); + this.logger = getLogger('user-store.ts'); } async update(id: number, fields: IUserUpdateFields): Promise { - await this.db(TABLE) - .where('id', id) - .update(mapUserToColumns(fields)); - return this.get({ id }); + await this.db(TABLE).where('id', id).update(mapUserToColumns(fields)); + return this.get(id); } async insert(user: ICreateUser): Promise { @@ -153,15 +134,13 @@ class UserStore { return users.map(rowToUser); } - async get(idQuery: IUserLookup): Promise { + async getByQuery(idQuery: IUserLookup): Promise { const row = await this.buildSelectUser(idQuery).first(USER_COLUMNS); return rowToUser(row); } async delete(id: number): Promise { - return this.db(TABLE) - .where({ id }) - .del(); + return this.db(TABLE).where({ id }).del(); } async getPasswordHash(userId: number): Promise { @@ -177,11 +156,9 @@ class UserStore { } async setPasswordHash(userId: number, passwordHash: string): Promise { - return this.db(TABLE) - .where('id', userId) - .update({ - password_hash: passwordHash, - }); + return this.db(TABLE).where('id', userId).update({ + password_hash: passwordHash, + }); } async incLoginAttempts(user: User): Promise { @@ -198,6 +175,22 @@ class UserStore { async deleteAll(): Promise { await this.db(TABLE).del(); } + + destroy(): void {} + + async exists(id: number): Promise { + const result = await this.db.raw( + `SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE id = ?) AS present`, + [id], + ); + const { present } = result.rows[0]; + return present; + } + + async get(id: number): Promise { + const row = await this.db(TABLE).where({ id }).first(); + return rowToUser(row); + } } module.exports = UserStore; diff --git a/src/lib/error/invalid-operation-error.ts b/src/lib/error/invalid-operation-error.ts index 5ca226f55c..260772a6ca 100644 --- a/src/lib/error/invalid-operation-error.ts +++ b/src/lib/error/invalid-operation-error.ts @@ -1,5 +1,3 @@ -'use strict'; - class InvalidOperationError extends Error { constructor(message: string) { super(); diff --git a/src/lib/error/name-exists-error.ts b/src/lib/error/name-exists-error.ts index 3ac37c5f2f..aa8da19c48 100644 --- a/src/lib/error/name-exists-error.ts +++ b/src/lib/error/name-exists-error.ts @@ -1,5 +1,3 @@ -'use strict'; - class NameExistsError extends Error { constructor(message: string) { super(); diff --git a/src/lib/event-differ.js b/src/lib/event-differ.js index a63eaec6df..b527b2834b 100644 --- a/src/lib/event-differ.js +++ b/src/lib/event-differ.js @@ -102,7 +102,7 @@ function baseTypeFor(event) { return event.type; } -const uniqueFieldForType = baseType => { +const uniqueFieldForType = (baseType) => { if (baseType === 'user') { return 'id'; } @@ -112,7 +112,7 @@ const uniqueFieldForType = baseType => { function groupByBaseTypeAndName(events) { const groups = {}; - events.forEach(event => { + events.forEach((event) => { const baseType = baseTypeFor(event); const uniqueField = uniqueFieldForType(baseType); @@ -129,10 +129,10 @@ function groupByBaseTypeAndName(events) { function eachConsecutiveEvent(events, callback) { const groups = groupByBaseTypeAndName(events); - Object.keys(groups).forEach(baseType => { + Object.keys(groups).forEach((baseType) => { const group = groups[baseType]; - Object.keys(group).forEach(name => { + Object.keys(group).forEach((name) => { const currentEvents = group[name]; let left; let right; diff --git a/src/lib/event-hook.test.js b/src/lib/event-hook.test.js index 5e6c52ed06..3ed0dd2ca1 100644 --- a/src/lib/event-hook.test.js +++ b/src/lib/event-hook.test.js @@ -22,7 +22,7 @@ beforeAll(() => { }); [FEATURE_CREATED, FEATURE_UPDATED, FEATURE_ARCHIVED, FEATURE_REVIVED].forEach( - feature => { + (feature) => { test(`should invoke hook on ${feature}`, () => { const data = { dataKey: feature }; eventStore.emit(feature, data); diff --git a/src/lib/event-hook.ts b/src/lib/event-hook.ts index 3fbef1287d..6b7363a986 100644 --- a/src/lib/event-hook.ts +++ b/src/lib/event-hook.ts @@ -11,16 +11,16 @@ export const addEventHook = ( eventHook: EventHook, eventStore: EventEmitter, ): void => { - eventStore.on(FEATURE_CREATED, data => { + eventStore.on(FEATURE_CREATED, (data) => { eventHook(FEATURE_CREATED, data); }); - eventStore.on(FEATURE_UPDATED, data => { + eventStore.on(FEATURE_UPDATED, (data) => { eventHook(FEATURE_UPDATED, data); }); - eventStore.on(FEATURE_ARCHIVED, data => { + eventStore.on(FEATURE_ARCHIVED, (data) => { eventHook(FEATURE_ARCHIVED, data); }); - eventStore.on(FEATURE_REVIVED, data => { + eventStore.on(FEATURE_REVIVED, (data) => { eventHook(FEATURE_REVIVED, data); }); }; diff --git a/src/lib/metrics.test.js b/src/lib/metrics.test.ts similarity index 68% rename from src/lib/metrics.test.js rename to src/lib/metrics.test.ts index f3e1875cc4..3ce2d1895c 100644 --- a/src/lib/metrics.test.js +++ b/src/lib/metrics.test.ts @@ -1,46 +1,36 @@ -'use strict'; - -const { EventEmitter } = require('events'); - -const eventBus = new EventEmitter(); - -const eventStore = new EventEmitter(); -const clientMetricsStore = new EventEmitter(); -const { register: prometheusRegister } = require('prom-client'); -const { createTestConfig } = require('../test/config/test-config'); -const { REQUEST_TIME, DB_TIME } = require('./metric-events'); -const { FEATURE_UPDATED } = require('./types/events'); -const { createMetricsMonitor } = require('./metrics'); +import { register } from 'prom-client'; +import EventEmitter from 'events'; +import { createTestConfig } from '../test/config/test-config'; +import { REQUEST_TIME, DB_TIME } from './metric-events'; +import { FEATURE_UPDATED } from './types/events'; +import { createMetricsMonitor } from './metrics'; +import createStores from '../test/fixtures/store'; const monitor = createMetricsMonitor(); - +const eventBus = new EventEmitter(); +const prometheusRegister = register; +let stores; beforeAll(() => { - const featureToggleStore = { - count: async () => 123, - }; const config = createTestConfig({ server: { serverMetrics: true, }, }); - const stores = { - eventStore, - clientMetricsStore, - featureToggleStore, - db: { - client: { - pool: { - min: 0, - max: 4, - numUsed: () => 2, - numFree: () => 2, - numPendingAcquires: () => 0, - numPendingCreates: () => 1, - }, + stores = createStores(); + const db = { + client: { + pool: { + min: 0, + max: 4, + numUsed: () => 2, + numFree: () => 2, + numPendingAcquires: () => 0, + numPendingCreates: () => 1, }, }, }; - monitor.startMonitoring(config, stores, '4.0.0', eventBus); + // @ts-ignore - We don't want a full knex implementation for our tests, it's enough that it actually yields the numbers we want. + monitor.startMonitoring(config, stores, '4.0.0', eventBus, db); }); afterAll(() => { @@ -57,12 +47,12 @@ test('should collect metrics for requests', async () => { const metrics = await prometheusRegister.metrics(); expect(metrics).toMatch( - /http_request_duration_milliseconds{quantile="0\.99",path="somePath",method="GET",status="200"} 1337/, + /http_request_duration_milliseconds{quantile="0\.99",path="somePath",method="GET",status="200"}.*1337/, ); }); test('should collect metrics for updated toggles', async () => { - eventStore.emit(FEATURE_UPDATED, { + stores.eventStore.emit(FEATURE_UPDATED, { data: { name: 'TestToggle' }, }); @@ -73,7 +63,7 @@ test('should collect metrics for updated toggles', async () => { }); test('should collect metrics for client metric reports', async () => { - clientMetricsStore.emit('metrics', { + stores.clientMetricsStore.emit('metrics', { bucket: { toggles: { TestToggle: { @@ -105,7 +95,7 @@ test('should collect metrics for db query timings', async () => { test('should collect metrics for feature toggle size', async () => { const metrics = await prometheusRegister.metrics(); - expect(metrics).toMatch(/feature_toggles_total{version="(.*)"} 123/); + expect(metrics).toMatch(/feature_toggles_total{version="(.*)"} 0/); }); test('Should collect metrics for database', async () => { diff --git a/src/lib/metrics.ts b/src/lib/metrics.ts index d13cbaeda5..38b3fc2c4f 100644 --- a/src/lib/metrics.ts +++ b/src/lib/metrics.ts @@ -31,6 +31,7 @@ export default class MetricsMonitor { stores: IUnleashStores, version: string, eventBus: EventEmitter, + db: Knex, ): Promise { if (!config.server.serverMetrics) { return; @@ -72,7 +73,9 @@ export default class MetricsMonitor { featureTogglesTotal.reset(); let togglesCount; try { - togglesCount = await featureToggleStore.count(); + togglesCount = await featureToggleStore.count({ + archived: false, + }); // eslint-disable-next-line no-empty } catch (e) {} @@ -110,7 +113,7 @@ export default class MetricsMonitor { featureToggleUpdateTotal.labels(data.name).inc(); }); - clientMetricsStore.on('metrics', m => { + clientMetricsStore.on('metrics', (m) => { // eslint-disable-next-line no-restricted-syntax for (const entry of Object.entries(m.bucket.toggles)) { featureToggleUsageTotal @@ -124,7 +127,7 @@ export default class MetricsMonitor { } }); - this.configureDbMetrics(stores.db, eventBus); + this.configureDbMetrics(db, eventBus); } stopMonitoring(): void { @@ -154,16 +157,14 @@ export default class MetricsMonitor { }); const dbPoolPendingCreates = new client.Gauge({ name: 'db_pool_pending_creates', - help: - 'how many asynchronous create calls are running in DB pool', + help: 'how many asynchronous create calls are running in DB pool', }); const dbPoolPendingAcquires = new client.Gauge({ name: 'db_pool_pending_acquires', - help: - 'how many acquires are waiting for a resource to be released in DB pool', + help: 'how many acquires are waiting for a resource to be released in DB pool', }); - eventBus.on(DB_POOL_UPDATE, data => { + eventBus.on(DB_POOL_UPDATE, (data) => { dbPoolFree.set(data.free); dbPoolUsed.set(data.used); dbPoolPendingCreates.set(data.pendingCreates); diff --git a/src/lib/middleware/content_type_checker.test.js b/src/lib/middleware/content_type_checker.test.ts similarity index 84% rename from src/lib/middleware/content_type_checker.test.js rename to src/lib/middleware/content_type_checker.test.ts index 43d8fd229a..9ba7865501 100644 --- a/src/lib/middleware/content_type_checker.test.js +++ b/src/lib/middleware/content_type_checker.test.ts @@ -1,7 +1,9 @@ -const requireContentType = require('./content_type_checker'); +import { Request, Response } from 'express'; +import requireContentType from './content_type_checker'; -const mockRequest = contentType => ({ - header: name => { +const mockRequest: (contentType: string) => Request = (contentType) => ({ + // @ts-ignore + header: (name) => { if (name === 'Content-Type') { return contentType; } @@ -9,8 +11,9 @@ const mockRequest = contentType => ({ }, }); -const returns415 = t => ({ - status: code => { +const returns415: (t: jest.Mock) => Response = (t) => ({ + // @ts-ignore + status: (code) => { expect(415).toBe(code); return { end: t, @@ -18,7 +21,8 @@ const returns415 = t => ({ }, }); -const expectNoCall = t => ({ +const expectNoCall: (t: jest.Mock) => Response = (t) => ({ + // @ts-ignore status: () => ({ end: () => expect(t).toHaveBeenCalledTimes(0), }), diff --git a/src/lib/middleware/content_type_checker.js b/src/lib/middleware/content_type_checker.ts similarity index 86% rename from src/lib/middleware/content_type_checker.js rename to src/lib/middleware/content_type_checker.ts index 3cfef8448c..2a45bd9856 100644 --- a/src/lib/middleware/content_type_checker.js +++ b/src/lib/middleware/content_type_checker.ts @@ -1,3 +1,5 @@ +import { RequestHandler } from 'express'; + const DEFAULT_ACCEPTED_CONTENT_TYPE = 'application/json'; /** @@ -7,7 +9,9 @@ const DEFAULT_ACCEPTED_CONTENT_TYPE = 'application/json'; * @param {String} acceptedContentTypes * @returns {function(Request, Response, NextFunction): void} */ -function requireContentType(...acceptedContentTypes) { +export default function requireContentType( + ...acceptedContentTypes: string[] +): RequestHandler { return (req, res, next) => { const contentType = req.header('Content-Type'); if ( diff --git a/src/lib/middleware/demo-authentication.js b/src/lib/middleware/demo-authentication.ts similarity index 60% rename from src/lib/middleware/demo-authentication.js rename to src/lib/middleware/demo-authentication.ts index ce2b477c7b..e061006050 100644 --- a/src/lib/middleware/demo-authentication.js +++ b/src/lib/middleware/demo-authentication.ts @@ -1,28 +1,42 @@ -const AuthenticationRequired = require('../types/authentication-required'); +import { Application } from 'express'; +import AuthenticationRequired from '../types/authentication-required'; +import { IUnleashServices } from '../types/services'; -function demoAuthentication(app, basePath = '', { userService }) { +function demoAuthentication( + app: Application, + basePath: string = '', + { userService }: Pick, +): void { app.post(`${basePath}/api/admin/login`, async (req, res) => { const { email } = req.body; const user = await userService.loginUserWithoutPassword(email, true); - req.session.user = user; + const session = req.session || {}; + // @ts-ignore + session.user = user; + // @ts-ignore + req.session = session; res.status(200) + // @ts-ignore .json(req.session.user) .end(); }); app.use(`${basePath}/api/admin/`, (req, res, next) => { + // @ts-ignore if (req.session.user && req.session.user.email) { + // @ts-ignore req.user = req.session.user; } next(); }); app.use(`${basePath}/api/admin/`, (req, res, next) => { + // @ts-ignore if (req.user) { return next(); } return res - .status('401') + .status(401) .json( new AuthenticationRequired({ path: `${basePath}/api/admin/login`, @@ -34,5 +48,4 @@ function demoAuthentication(app, basePath = '', { userService }) { .end(); }); } - -module.exports = demoAuthentication; +export default demoAuthentication; diff --git a/src/lib/middleware/no-authentication.js b/src/lib/middleware/no-authentication.js deleted file mode 100644 index 03d16916a3..0000000000 --- a/src/lib/middleware/no-authentication.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; - -const { ADMIN } = require('../types/permissions'); -const ApiUser = require('../types/api-user'); - -function noneAuthentication(basePath = '', app) { - app.use(`${basePath}/api/admin/`, (req, res, next) => { - if (!req.user) { - req.user = new ApiUser({ - username: 'unknown', - permissions: [ADMIN], - }); - } - next(); - }); -} - -module.exports = noneAuthentication; diff --git a/src/lib/middleware/no-authentication.test.js b/src/lib/middleware/no-authentication.test.ts similarity index 53% rename from src/lib/middleware/no-authentication.test.js rename to src/lib/middleware/no-authentication.test.ts index 605b069a2d..9466191372 100644 --- a/src/lib/middleware/no-authentication.test.js +++ b/src/lib/middleware/no-authentication.test.ts @@ -1,28 +1,24 @@ -'use strict'; - -const supertest = require('supertest'); -const express = require('express'); -const noAuthentication = require('./no-authentication'); +import supertest from 'supertest'; +import express from 'express'; +import noAuthentication from './no-authentication'; +import { IUserRequest } from '../routes/admin-api/user'; test('should add dummy user object to all requests', () => { expect.assertions(1); const app = express(); noAuthentication('', app); - app.get('/api/admin/test', (req, res) => { + app.get('/api/admin/test', (req: IUserRequest, res) => { const user = { ...req.user }; - return res - .status(200) - .json(user) - .end(); + return res.status(200).json(user).end(); }); const request = supertest(app); return request .get('/api/admin/test') .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.username === 'unknown').toBe(true); }); }); diff --git a/src/lib/middleware/no-authentication.ts b/src/lib/middleware/no-authentication.ts new file mode 100644 index 0000000000..2696b09b86 --- /dev/null +++ b/src/lib/middleware/no-authentication.ts @@ -0,0 +1,18 @@ +import { Application } from 'express'; +import { ADMIN } from '../types/permissions'; +import ApiUser from '../types/api-user'; + +function noneAuthentication(basePath = '', app: Application): void { + app.use(`${basePath}/api/admin/`, (req, res, next) => { + // @ts-ignore + if (!req.user) { + // @ts-ignore + req.user = new ApiUser({ + username: 'unknown', + permissions: [ADMIN], + }); + } + next(); + }); +} +export default noneAuthentication; diff --git a/src/lib/middleware/oss-authentication.js b/src/lib/middleware/oss-authentication.js deleted file mode 100644 index 2f49700b95..0000000000 --- a/src/lib/middleware/oss-authentication.js +++ /dev/null @@ -1,31 +0,0 @@ -const AuthenticationRequired = require('../types/authentication-required'); - -function ossAuthHook(app, config) { - const { baseUriPath } = config.server; - - const generateAuthResponse = async () => - new AuthenticationRequired({ - type: 'password', - path: `${baseUriPath}/auth/simple/login`, - message: 'You must sign in order to use Unleash', - }); - - app.use(`${baseUriPath}/api`, async (req, res, next) => { - if (req.session && req.session.user) { - req.user = req.session.user; - return next(); - } - if (req.user) { - return next(); - } - if (req.header('authorization')) { - // API clients should get 401 without body - return res.sendStatus(401); - } - // Admin UI users should get auth-response - const authRequired = await generateAuthResponse(); - return res.status(401).json(authRequired); - }); -} - -module.exports = ossAuthHook; diff --git a/src/lib/middleware/oss-authentication.test.js b/src/lib/middleware/oss-authentication.test.ts similarity index 57% rename from src/lib/middleware/oss-authentication.test.js rename to src/lib/middleware/oss-authentication.test.ts index d7438ceabd..83897dff75 100644 --- a/src/lib/middleware/oss-authentication.test.js +++ b/src/lib/middleware/oss-authentication.test.ts @@ -1,14 +1,13 @@ -'use strict'; +import supertest from 'supertest'; +import { EventEmitter } from 'events'; +import { createServices } from '../services'; +import { createTestConfig } from '../../test/config/test-config'; -const supertest = require('supertest'); -const { EventEmitter } = require('events'); -const { createServices } = require('../services'); -const { createTestConfig } = require('../../test/config/test-config'); - -const store = require('../../test/fixtures/store'); -const ossAuth = require('./oss-authentication'); -const getApp = require('../app'); -const { User } = require('../server-impl'); +import createStores from '../../test/fixtures/store'; +import ossAuth from './oss-authentication'; +import getApp from '../app'; +import User from '../types/user'; +import sessionDb from './session-db'; const eventBus = new EventEmitter(); @@ -16,19 +15,18 @@ function getSetup(preRouterHook) { const base = `/random${Math.round(Math.random() * 1000)}`; const config = createTestConfig({ server: { baseUriPath: base }, - preRouterHook: _app => { + preRouterHook: (_app) => { preRouterHook(_app); - ossAuth(_app, { server: { baseUriPath: base } }); + ossAuth(_app, base); _app.get(`${base}/api/protectedResource`, (req, res) => { - res.status(200) - .json({ message: 'OK' }) - .end(); + res.status(200).json({ message: 'OK' }).end(); }); }, }); - const stores = store.createStores(); + const stores = createStores(); const services = createServices(stores, config); - const app = getApp(config, stores, services, eventBus); + const unleashSession = sessionDb(config, undefined); + const app = getApp(config, stores, services, eventBus, unleashSession); return { base, @@ -46,7 +44,7 @@ test('should return 401 when missing user', () => { test('should return 200 when user exists', () => { expect.assertions(0); const user = new User({ id: 1, email: 'some@mail.com' }); - const { base, request } = getSetup(app => + const { base, request } = getSetup((app) => app.use((req, res, next) => { req.user = user; next(); diff --git a/src/lib/middleware/oss-authentication.ts b/src/lib/middleware/oss-authentication.ts new file mode 100644 index 0000000000..01a33e8692 --- /dev/null +++ b/src/lib/middleware/oss-authentication.ts @@ -0,0 +1,35 @@ +import { Application, NextFunction, Request, Response } from 'express'; +import AuthenticationRequired from '../types/authentication-required'; + +function ossAuthHook(app: Application, baseUriPath: string): void { + const generateAuthResponse = async () => + new AuthenticationRequired({ + type: 'password', + path: `${baseUriPath}/auth/simple/login`, + message: 'You must sign in order to use Unleash', + }); + + app.use( + `${baseUriPath}/api`, + async (req: Request, res: Response, next: NextFunction) => { + // @ts-ignore + if (req.session && req.session.user) { + // @ts-ignore + req.user = req.session.user; + return next(); + } + // @ts-ignore + if (req.user) { + return next(); + } + if (req.header('authorization')) { + // API clients should get 401 without body + return res.sendStatus(401); + } + // Admin UI users should get auth-response + const authRequired = await generateAuthResponse(); + return res.status(401).json(authRequired); + }, + ); +} +export default ossAuthHook; diff --git a/src/lib/middleware/rbac-middleware.test.ts b/src/lib/middleware/rbac-middleware.test.ts index d83f47bba0..d9cade938c 100644 --- a/src/lib/middleware/rbac-middleware.test.ts +++ b/src/lib/middleware/rbac-middleware.test.ts @@ -1,16 +1,17 @@ import rbacMiddleware from './rbac-middleware'; -import ffStore from '../../test/fixtures/fake-feature-toggle-store'; import User from '../types/user'; import * as perms from '../types/permissions'; import { IUnleashConfig } from '../types/option'; import { createTestConfig } from '../../test/config/test-config'; import ApiUser from '../types/api-user'; +import { IFeatureToggleStore } from '../types/stores/feature-toggle-store'; +import FakeFeatureToggleStore from '../../test/fixtures/fake-feature-toggle-store'; let config: IUnleashConfig; -let featureToggleStore: any; +let featureToggleStore: IFeatureToggleStore; beforeEach(() => { - featureToggleStore = ffStore(); + featureToggleStore = new FakeFeatureToggleStore(); config = createTestConfig(); }); @@ -206,7 +207,6 @@ test('should lookup projectId from feature toggle', async () => { perms.UPDATE_FEATURE, projectId, ); - expect(featureToggleStore.getProjectId.mock.calls[0][0]).toBe(featureName); }); test('should lookup projectId from data', async () => { diff --git a/src/lib/middleware/request-logger.js b/src/lib/middleware/request-logger.ts similarity index 63% rename from src/lib/middleware/request-logger.js rename to src/lib/middleware/request-logger.ts index 33cc806822..00ca7e089d 100644 --- a/src/lib/middleware/request-logger.js +++ b/src/lib/middleware/request-logger.ts @@ -1,8 +1,8 @@ -'use strict'; +import url from 'url'; +import { RequestHandler } from 'express'; +import { IUnleashConfig } from '../types/option'; -const url = require('url'); - -module.exports = function(config) { +const requestLogger: (config: IUnleashConfig) => RequestHandler = (config) => { const logger = config.getLogger('HTTP'); const enable = config.server.enableRequestLogger; return (req, res, next) => { @@ -15,3 +15,5 @@ module.exports = function(config) { next(); }; }; + +export default requestLogger; diff --git a/src/lib/middleware/secure-headers.js b/src/lib/middleware/secure-headers.ts similarity index 80% rename from src/lib/middleware/secure-headers.js rename to src/lib/middleware/secure-headers.ts index fef768db70..a9119e0654 100644 --- a/src/lib/middleware/secure-headers.js +++ b/src/lib/middleware/secure-headers.ts @@ -1,6 +1,8 @@ -const helmet = require('helmet'); +import helmet from 'helmet'; +import { RequestHandler } from 'express'; +import { IUnleashConfig } from '../types/option'; -module.exports = function(config) { +const secureHeaders: (config: IUnleashConfig) => RequestHandler = (config) => { if (config.secureHeaders) { return helmet({ hsts: { @@ -33,3 +35,5 @@ module.exports = function(config) { next(); }; }; + +export default secureHeaders; diff --git a/src/lib/middleware/session-db.ts b/src/lib/middleware/session-db.ts index e46dfbe7e2..c70ad36456 100644 --- a/src/lib/middleware/session-db.ts +++ b/src/lib/middleware/session-db.ts @@ -1,28 +1,30 @@ +import { Knex } from 'knex'; +import session from 'express-session'; +import knexSessionStore from 'connect-session-knex'; +import { RequestHandler } from 'express'; import { IUnleashConfig } from '../types/option'; -import { IUnleashStores } from '../types/stores'; - -const session = require('express-session'); -const KnexSessionStore = require('connect-session-knex')(session); const TWO_DAYS = 48 * 60 * 60 * 1000; const HOUR = 60 * 60 * 1000; function sessionDb( config: Pick, - stores: Pick, -): any { + knex: Knex, +): RequestHandler { let store; const { db } = config.session; const age = config.session.ttlHours * HOUR || TWO_DAYS; + const KnexSessionStore = knexSessionStore(session); if (db) { store = new KnexSessionStore({ - knex: stores.db, tablename: 'unleash_session', createtable: false, + // @ts-ignore + knex, }); } else { store = new session.MemoryStore(); } - const sessionMiddleware = session({ + return session({ name: 'unleash-session', rolling: false, resave: false, @@ -38,7 +40,5 @@ function sessionDb( maxAge: age, }, }); - return (req, res, next) => sessionMiddleware(req, res, next); } export default sessionDb; -module.exports = sessionDb; diff --git a/src/lib/routes/admin-api/addon.ts b/src/lib/routes/admin-api/addon.ts index b5bcc5b5dd..73b6bfa3ba 100644 --- a/src/lib/routes/admin-api/addon.ts +++ b/src/lib/routes/admin-api/addon.ts @@ -1,5 +1,3 @@ -'use strict'; - import { Request, Response } from 'express'; import Controller from '../controller'; import { IUnleashConfig } from '../../types/option'; @@ -9,7 +7,11 @@ import AddonService from '../../services/addon-service'; import extractUser from '../../extract-user'; import { handleErrors } from './util'; -import { CREATE_ADDON, UPDATE_ADDON, DELETE_ADDON } from '../../types/permissions'; +import { + CREATE_ADDON, + UPDATE_ADDON, + DELETE_ADDON, +} from '../../types/permissions'; class AddonController extends Controller { private logger: Logger; @@ -21,7 +23,7 @@ class AddonController extends Controller { { addonService }: Pick, ) { super(config); - this.logger = config.getLogger('/admin-api/addon.js'); + this.logger = config.getLogger('/admin-api/addon.ts'); this.addonService = addonService; this.get('/', this.getAddons); @@ -34,14 +36,17 @@ class AddonController extends Controller { async getAddons(req: Request, res: Response): Promise { try { const addons = await this.addonService.getAddons(); - const providers = await this.addonService.getProviderDefinition(); + const providers = this.addonService.getProviderDefinitions(); res.json({ addons, providers }); } catch (error) { handleErrors(res, this.logger, error); } } - async getAddon(req: Request, res: Response): Promise { + async getAddon( + req: Request<{ id: number }, any, any, any>, + res: Response, + ): Promise { const { id } = req.params; try { const addon = await this.addonService.getAddon(id); @@ -51,7 +56,10 @@ class AddonController extends Controller { } } - async updateAddon(req: Request, res: Response): Promise { + async updateAddon( + req: Request<{ id: number }, any, any, any>, + res: Response, + ): Promise { const { id } = req.params; const createdBy = extractUser(req); const data = req.body; @@ -79,7 +87,10 @@ class AddonController extends Controller { } } - async deleteAddon(req: Request, res: Response): Promise { + async deleteAddon( + req: Request<{ id: number }, any, any, any>, + res: Response, + ): Promise { const { id } = req.params; const username = extractUser(req); try { diff --git a/src/lib/routes/admin-api/api-token-controller.ts b/src/lib/routes/admin-api/api-token-controller.ts index f2550b3af1..a016355579 100644 --- a/src/lib/routes/admin-api/api-token-controller.ts +++ b/src/lib/routes/admin-api/api-token-controller.ts @@ -9,11 +9,11 @@ import { } from '../../types/permissions'; import { ApiTokenService } from '../../services/api-token-service'; import { Logger } from '../../logger'; -import { ApiTokenType } from '../../db/api-token-store'; import { AccessService } from '../../services/access-service'; import { IAuthRequest } from '../unleash-types'; import User from '../../types/user'; import { IUnleashConfig } from '../../types/option'; +import { ApiTokenType } from '../../types/stores/api-token-store'; interface IServices { apiTokenService: ApiTokenService; @@ -57,7 +57,7 @@ class ApiTokenController extends Controller { res.json({ tokens }); } else { const filteredTokens = tokens.filter( - t => !(t.type === ApiTokenType.ADMIN), + (t) => !(t.type === ApiTokenType.ADMIN), ); res.json({ tokens: filteredTokens }); } diff --git a/src/lib/routes/admin-api/archive.ts b/src/lib/routes/admin-api/archive.ts index 03979b4d97..5a57837e80 100644 --- a/src/lib/routes/admin-api/archive.ts +++ b/src/lib/routes/admin-api/archive.ts @@ -37,9 +37,8 @@ export default class ArchiveController extends Controller { // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async getArchivedFeatures(req, res): Promise { try { - const features = await this.featureService.getMetadataForAllFeatures( - true, - ); + const features = + await this.featureService.getMetadataForAllFeatures(true); res.json({ version: 2, features }); } catch (err) { handleErrors(res, this.logger, err); diff --git a/src/lib/routes/admin-api/bootstrap-controller.ts b/src/lib/routes/admin-api/bootstrap-controller.ts index d51192851b..be40fd1b67 100644 --- a/src/lib/routes/admin-api/bootstrap-controller.ts +++ b/src/lib/routes/admin-api/bootstrap-controller.ts @@ -3,15 +3,9 @@ import Controller from '../controller'; import { AuthedRequest } from '../../types/core'; import { Logger } from '../../logger'; import ContextService from '../../services/context-service'; -import FeatureTypeStore, { IFeatureType } from '../../db/feature-type-store'; import TagTypeService from '../../services/tag-type-service'; import StrategyService from '../../services/strategy-service'; import ProjectService from '../../services/project-service'; -import { IContextField } from '../../db/context-field-store'; -import { ITagType } from '../../db/tag-type-store'; -import { IProject } from '../../db/project-store'; -import { IStrategy } from '../../db/strategy-store'; -import { IUserPermission } from '../../db/access-store'; import { AccessService } from '../../services/access-service'; import { EmailService } from '../../services/email-service'; import { IUnleashConfig } from '../../types/option'; @@ -19,6 +13,12 @@ import { IUnleashServices } from '../../types/services'; import VersionService from '../../services/version-service'; import FeatureTypeService from '../../services/feature-type-service'; import version from '../../util/version'; +import { IContextField } from '../../types/stores/context-field-store'; +import { IFeatureType } from '../../types/stores/feature-type-store'; +import { ITagType } from '../../types/stores/tag-type-store'; +import { IStrategy } from '../../types/stores/strategy-store'; +import { IProject } from '../../types/stores/project-store'; +import { IUserPermission } from '../../types/stores/access-store'; class BootstrapController extends Controller { private logger: Logger; diff --git a/src/lib/routes/admin-api/config.test.js b/src/lib/routes/admin-api/config.test.ts similarity index 75% rename from src/lib/routes/admin-api/config.test.js rename to src/lib/routes/admin-api/config.test.ts index 542563bce1..def13f6084 100644 --- a/src/lib/routes/admin-api/config.test.js +++ b/src/lib/routes/admin-api/config.test.ts @@ -1,12 +1,10 @@ -'use strict'; +import supertest from 'supertest'; +import { EventEmitter } from 'events'; +import { createTestConfig } from '../../../test/config/test-config'; -const supertest = require('supertest'); -const { EventEmitter } = require('events'); -const { createServices } = require('../../services'); -const { createTestConfig } = require('../../../test/config/test-config'); - -const store = require('../../../test/fixtures/store'); -const getApp = require('../../app'); +import createStores from '../../../test/fixtures/store'; +import getApp from '../../app'; +import { createServices } from '../../services'; const eventBus = new EventEmitter(); @@ -21,7 +19,7 @@ function getSetup() { server: { baseUriPath: base }, ui: uiConfig, }); - const stores = store.createStores(); + const stores = createStores(); const services = createServices(stores, config); const app = getApp(config, stores, services, eventBus); @@ -57,7 +55,7 @@ test('should get ui config', () => { .get(`${base}/api/admin/ui-config`) .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.slogan === 'hello').toBe(true); expect(res.body.headerBackground === 'red').toBe(true); }); diff --git a/src/lib/routes/admin-api/config.ts b/src/lib/routes/admin-api/config.ts index 93ebc417bd..93f7575816 100644 --- a/src/lib/routes/admin-api/config.ts +++ b/src/lib/routes/admin-api/config.ts @@ -1,5 +1,3 @@ -'use strict'; - import { Request, Response } from 'express'; import { IUnleashServices } from '../../types/services'; import { IUnleashConfig } from '../../types/option'; diff --git a/src/lib/routes/admin-api/context.test.js b/src/lib/routes/admin-api/context.test.ts similarity index 88% rename from src/lib/routes/admin-api/context.test.js rename to src/lib/routes/admin-api/context.test.ts index 95e9ac8349..62a744ec41 100644 --- a/src/lib/routes/admin-api/context.test.js +++ b/src/lib/routes/admin-api/context.test.ts @@ -1,12 +1,10 @@ -'use strict'; - -const supertest = require('supertest'); -const { EventEmitter } = require('events'); -const { createTestConfig } = require('../../../test/config/test-config'); -const store = require('../../../test/fixtures/store'); -const { createServices } = require('../../services'); -const permissions = require('../../../test/fixtures/permissions'); -const getApp = require('../../app'); +import supertest from 'supertest'; +import { EventEmitter } from 'events'; +import { createTestConfig } from '../../../test/config/test-config'; +import createStores from '../../../test/fixtures/store'; +import { createServices } from '../../services'; +import permissions from '../../../test/fixtures/permissions'; +import getApp from '../../app'; const eventBus = new EventEmitter(); @@ -17,7 +15,7 @@ function getSetup() { preHook: perms.hook, server: { baseUriPath: base }, }); - const stores = store.createStores(); + const stores = createStores(); const services = createServices(stores, config); const app = getApp(config, stores, services, eventBus); @@ -54,9 +52,9 @@ test('should get all context definitions', () => { .get(`${base}/api/admin/context`) .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.length === 3).toBe(true); - const envField = res.body.find(c => c.name === 'environment'); + const envField = res.body.find((c) => c.name === 'environment'); expect(envField.name === 'environment').toBe(true); }); }); @@ -67,7 +65,7 @@ test('should get context definition', () => { .get(`${base}/api/admin/context/userId`) .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.name).toBe('userId'); }); }); diff --git a/src/lib/routes/admin-api/context.ts b/src/lib/routes/admin-api/context.ts index b00118b805..62be89daed 100644 --- a/src/lib/routes/admin-api/context.ts +++ b/src/lib/routes/admin-api/context.ts @@ -47,9 +47,7 @@ class ContextController extends Controller { async getContextFields(req: Request, res: Response): Promise { try { const fields = await this.contextService.getAll(); - res.status(200) - .json(fields) - .end(); + res.status(200).json(fields).end(); } catch (e) { handleErrors(res, this.logger, e); } diff --git a/src/lib/routes/admin-api/email.test.js b/src/lib/routes/admin-api/email.test.ts similarity index 72% rename from src/lib/routes/admin-api/email.test.js rename to src/lib/routes/admin-api/email.test.ts index 8413d2b9fd..41c51168a1 100644 --- a/src/lib/routes/admin-api/email.test.js +++ b/src/lib/routes/admin-api/email.test.ts @@ -1,18 +1,16 @@ -'use strict'; - -const supertest = require('supertest'); -const { EventEmitter } = require('events'); -const { createTestConfig } = require('../../../test/config/test-config'); -const store = require('../../../test/fixtures/store'); -const { createServices } = require('../../services'); -const permissions = require('../../../test/fixtures/permissions'); -const getApp = require('../../app'); +import supertest from 'supertest'; +import { EventEmitter } from 'events'; +import { createTestConfig } from '../../../test/config/test-config'; +import createStores from '../../../test/fixtures/store'; +import { createServices } from '../../services'; +import permissions from '../../../test/fixtures/permissions'; +import getApp from '../../app'; const eventBus = new EventEmitter(); function getSetup() { const base = `/random${Math.round(Math.random() * 1000)}`; - const stores = store.createStores(); + const stores = createStores(); const perms = permissions(); const config = createTestConfig({ preHook: perms.hook, @@ -37,7 +35,7 @@ test('should render html preview of template', () => { ) .expect('Content-Type', /html/) .expect(200) - .expect(res => 'Test Test' in res.body); + .expect((res) => 'Test Test' in res.body); }); test('should render text preview of template', () => { @@ -49,7 +47,7 @@ test('should render text preview of template', () => { ) .expect('Content-Type', /plain/) .expect(200) - .expect(res => 'Test Test' in res.body); + .expect((res) => 'Test Test' in res.body); }); test('Requesting a non-existing template should yield 404', () => { diff --git a/src/lib/routes/admin-api/event.ts b/src/lib/routes/admin-api/event.ts index 32d79cd8f0..d025011d4a 100644 --- a/src/lib/routes/admin-api/event.ts +++ b/src/lib/routes/admin-api/event.ts @@ -1,5 +1,3 @@ -'use strict'; - import { handleErrors } from './util'; import { IUnleashConfig } from '../../types/option'; import { IUnleashServices } from '../../types/services'; diff --git a/src/lib/routes/admin-api/events.test.js b/src/lib/routes/admin-api/events.test.ts similarity index 65% rename from src/lib/routes/admin-api/events.test.js rename to src/lib/routes/admin-api/events.test.ts index b232ae62ab..d455edfbd3 100644 --- a/src/lib/routes/admin-api/events.test.js +++ b/src/lib/routes/admin-api/events.test.ts @@ -1,19 +1,17 @@ -'use strict'; +import supertest from 'supertest'; +import { EventEmitter } from 'events'; +import { createServices } from '../../services'; +import { createTestConfig } from '../../../test/config/test-config'; -const supertest = require('supertest'); -const { EventEmitter } = require('events'); -const { createServices } = require('../../services'); -const { createTestConfig } = require('../../../test/config/test-config'); +import createStores from '../../../test/fixtures/store'; -const store = require('../../../test/fixtures/store'); - -const getApp = require('../../app'); +import getApp from '../../app'; const eventBus = new EventEmitter(); function getSetup() { const base = `/random${Math.round(Math.random() * 1000)}`; - const stores = store.createStores(); + const stores = createStores(); const config = createTestConfig({ server: { baseUriPath: base }, }); @@ -30,7 +28,7 @@ test('should get empty events list via admin', () => { .get(`${base}/api/admin/events`) .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.events.length === 0).toBe(true); }); }); diff --git a/src/lib/routes/admin-api/feature-type.ts b/src/lib/routes/admin-api/feature-type.ts index 1dd529de14..2e0751231e 100644 --- a/src/lib/routes/admin-api/feature-type.ts +++ b/src/lib/routes/admin-api/feature-type.ts @@ -1,5 +1,4 @@ -'use strict'; - +import { Request, Response } from 'express'; import { handleErrors } from './util'; import { IUnleashServices } from '../../types/services'; import FeatureTypeService from '../../services/feature-type-service'; @@ -26,7 +25,7 @@ export default class FeatureTypeController extends Controller { this.get('/', this.getAllFeatureTypes); } - async getAllFeatureTypes(req, res) { + async getAllFeatureTypes(req: Request, res: Response): Promise { try { const types = await this.featureTypeService.getAll(); res.json({ version, types }); diff --git a/src/lib/routes/admin-api/feature.ts b/src/lib/routes/admin-api/feature.ts index a0916d4e1b..dd55dbfc55 100644 --- a/src/lib/routes/admin-api/feature.ts +++ b/src/lib/routes/admin-api/feature.ts @@ -34,8 +34,8 @@ class FeatureController extends Controller { featureTagService, featureToggleServiceV2, }: Pick< - IUnleashServices, - 'featureTagService' | 'featureToggleServiceV2' + IUnleashServices, + 'featureTagService' | 'featureToggleServiceV2' >, ) { super(config); @@ -87,7 +87,7 @@ class FeatureController extends Controller { namePrefix, }); if (query.tag) { - query.tag = query.tag.map(q => q.split(':')); + query.tag = query.tag.map((q) => q.split(':')); } return query; } @@ -113,7 +113,7 @@ class FeatureController extends Controller { const name = req.params.featureName; const feature = await this.featureService2.getFeatureToggle(name); const strategies = - feature.environments.find(e => e.name === GLOBAL_ENV) + feature.environments.find((e) => e.name === GLOBAL_ENV) ?.strategies || []; res.json({ ...feature, @@ -186,13 +186,14 @@ class FeatureController extends Controller { try { const validatedToggle = await featureSchema.validateAsync(toggle); const { enabled } = validatedToggle; - const createdFeature = await this.featureService2.createFeatureToggle( - validatedToggle.project, - validatedToggle, - userName, - ); + const createdFeature = + await this.featureService2.createFeatureToggle( + validatedToggle.project, + validatedToggle, + userName, + ); const strategies = await Promise.all( - toggle.strategies.map(async s => + toggle.strategies.map(async (s) => this.featureService2.createStrategy( s, createdFeature.project, @@ -248,7 +249,7 @@ class FeatureController extends Controller { let strategies; if (updatedFeature.strategies) { strategies = await Promise.all( - updatedFeature.strategies.map(async s => + updatedFeature.strategies.map(async (s) => this.featureService2.createStrategy( s, projectId, diff --git a/src/lib/routes/admin-api/metrics.test.js b/src/lib/routes/admin-api/metrics.test.ts similarity index 89% rename from src/lib/routes/admin-api/metrics.test.js rename to src/lib/routes/admin-api/metrics.test.ts index 3434e0421e..3232ea58f3 100644 --- a/src/lib/routes/admin-api/metrics.test.js +++ b/src/lib/routes/admin-api/metrics.test.ts @@ -1,17 +1,15 @@ -'use strict'; - -const supertest = require('supertest'); -const { EventEmitter } = require('events'); -const store = require('../../../test/fixtures/store'); -const permissions = require('../../../test/fixtures/permissions'); -const getApp = require('../../app'); -const { createTestConfig } = require('../../../test/config/test-config'); -const { createServices } = require('../../services'); +import supertest from 'supertest'; +import { EventEmitter } from 'events'; +import createStores from '../../../test/fixtures/store'; +import permissions from '../../../test/fixtures/permissions'; +import getApp from '../../app'; +import { createTestConfig } from '../../../test/config/test-config'; +import { createServices } from '../../services'; const eventBus = new EventEmitter(); function getSetup() { - const stores = store.createStores(); + const stores = createStores(); const perms = permissions(); const config = createTestConfig({ preRouterHook: perms.hook, @@ -51,7 +49,7 @@ test('should return seen toggles even when there is nothing', () => { return request .get('/api/admin/metrics/seen-toggles') .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.length === 0).toBe(true); }); }); @@ -75,7 +73,7 @@ test('should return list of seen-toggles per app', () => { return request .get('/api/admin/metrics/seen-toggles') .expect(200) - .expect(res => { + .expect((res) => { const seenAppsWithToggles = res.body; expect(seenAppsWithToggles.length === 1).toBe(true); expect(seenAppsWithToggles[0].appName === appName).toBe(true); @@ -107,7 +105,7 @@ test('should return metrics for all toggles', () => { return request .get('/api/admin/metrics/feature-toggles') .expect(200) - .expect(res => { + .expect((res) => { const metrics = res.body; expect(metrics.lastHour !== undefined).toBe(true); expect(metrics.lastMinute !== undefined).toBe(true); @@ -120,7 +118,7 @@ test('should return empty list of client applications', () => { return request .get('/api/admin/metrics/applications') .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.applications.length === 0).toBe(true); }); }); @@ -134,7 +132,7 @@ test('should return applications', () => { return request .get('/api/admin/metrics/applications/') .expect(200) - .expect(res => { + .expect((res) => { const metrics = res.body; expect(metrics.applications.length === 1).toBe(true); expect(metrics.applications[0].appName === appName).toBe(true); diff --git a/src/lib/routes/admin-api/metrics.ts b/src/lib/routes/admin-api/metrics.ts index 21eb1a8b46..97b131db15 100644 --- a/src/lib/routes/admin-api/metrics.ts +++ b/src/lib/routes/admin-api/metrics.ts @@ -19,7 +19,7 @@ class MetricsController extends Controller { }: Pick, ) { super(config); - this.logger = config.getLogger('/admin-api/metrics.js'); + this.logger = config.getLogger('/admin-api/metrics.ts'); this.metrics = clientMetricsService; diff --git a/src/lib/routes/admin-api/project/environments.ts b/src/lib/routes/admin-api/project/environments.ts index 84f2ce3d27..9c5b0c3ccd 100644 --- a/src/lib/routes/admin-api/project/environments.ts +++ b/src/lib/routes/admin-api/project/environments.ts @@ -5,7 +5,7 @@ import { IUnleashServices } from '../../../types/services'; import { Logger } from '../../../logger'; import EnvironmentService from '../../../services/environment-service'; import { handleErrors } from '../util'; -import { UPDATE_FEATURE, UPDATE_PROJECT } from '../../../types/permissions'; +import { UPDATE_PROJECT } from '../../../types/permissions'; const PREFIX = '/:projectId/environments'; @@ -41,10 +41,10 @@ export default class EnvironmentsController extends Controller { async addEnvironmentToProject( req: Request< - Omit, - any, - EnvironmentBody, - any + Omit, + any, + EnvironmentBody, + any >, res: Response, ): Promise { diff --git a/src/lib/routes/admin-api/project/features.ts b/src/lib/routes/admin-api/project/features.ts index 3c1e87d244..8c1ce14dd6 100644 --- a/src/lib/routes/admin-api/project/features.ts +++ b/src/lib/routes/admin-api/project/features.ts @@ -7,9 +7,7 @@ import { Logger } from '../../../logger'; import { CREATE_FEATURE, UPDATE_FEATURE } from '../../../types/permissions'; import { FeatureToggleDTO, - IArchivedQuery, IConstraint, - IProjectParam, IStrategyConfig, } from '../../../types/model'; import { handleErrors } from '../util'; @@ -43,7 +41,7 @@ interface StrategyUpdateBody { const PATH_PREFIX = '/:projectId/features/:featureName'; type ProjectFeaturesServices = Pick< -IUnleashServices, + IUnleashServices, 'featureToggleServiceV2' | 'projectHealthService' >; @@ -147,11 +145,12 @@ export default class ProjectFeaturesController extends Controller { ): Promise { const { environment, featureName, projectId } = req.params; try { - const environmentInfo = await this.featureService.getEnvironmentInfo( - projectId, - environment, - featureName, - ); + const environmentInfo = + await this.featureService.getEnvironmentInfo( + projectId, + environment, + featureName, + ); res.status(200).json(environmentInfo); } catch (e) { handleErrors(res, this.logger, e); @@ -231,11 +230,12 @@ export default class ProjectFeaturesController extends Controller { ): Promise { const { projectId, featureName, environment } = req.params; try { - const featureStrategies = await this.featureService.getStrategiesForEnvironment( - projectId, - featureName, - environment, - ); + const featureStrategies = + await this.featureService.getStrategiesForEnvironment( + projectId, + featureName, + environment, + ); res.status(200).json(featureStrategies); } catch (e) { handleErrors(res, this.logger, e); diff --git a/src/lib/routes/admin-api/project/health-report.ts b/src/lib/routes/admin-api/project/health-report.ts index 417f819c1b..b62addb4da 100644 --- a/src/lib/routes/admin-api/project/health-report.ts +++ b/src/lib/routes/admin-api/project/health-report.ts @@ -48,9 +48,10 @@ export default class ProjectHealthReport extends Controller { ): Promise { const { projectId } = req.params; try { - const overview = await this.projectHealthService.getProjectHealthReport( - projectId, - ); + const overview = + await this.projectHealthService.getProjectHealthReport( + projectId, + ); res.json({ version: 2, ...overview, diff --git a/src/lib/routes/admin-api/state.ts b/src/lib/routes/admin-api/state.ts index 8b255e8fe1..c9046c724a 100644 --- a/src/lib/routes/admin-api/state.ts +++ b/src/lib/routes/admin-api/state.ts @@ -1,5 +1,3 @@ -'use strict'; - import * as mime from 'mime'; import YAML from 'js-yaml'; import moment from 'moment'; @@ -84,6 +82,7 @@ class StateController extends Controller { ); const includeProjects = paramToBool(req.query.projects, true); const includeTags = paramToBool(req.query.tags, true); + const includeEnvironments = paramToBool(req.query.environments, true); try { const data = await this.stateService.export({ @@ -91,6 +90,7 @@ class StateController extends Controller { includeFeatureToggles, includeProjects, includeTags, + includeEnvironments, }); const timestamp = moment().format('YYYY-MM-DD_HH-mm-ss'); if (format === 'yaml') { diff --git a/src/lib/routes/admin-api/strategy.test.js b/src/lib/routes/admin-api/strategy.test.ts similarity index 84% rename from src/lib/routes/admin-api/strategy.test.js rename to src/lib/routes/admin-api/strategy.test.ts index 8faaaeea8b..914efe4ad7 100644 --- a/src/lib/routes/admin-api/strategy.test.js +++ b/src/lib/routes/admin-api/strategy.test.ts @@ -1,13 +1,11 @@ -'use strict'; - -const supertest = require('supertest'); -const { EventEmitter } = require('events'); -const { createTestConfig } = require('../../../test/config/test-config'); -const store = require('../../../test/fixtures/store'); -const permissions = require('../../../test/fixtures/permissions'); -const getLogger = require('../../../test/fixtures/no-logger'); -const getApp = require('../../app'); -const { createServices } = require('../../services'); +import supertest from 'supertest'; +import { EventEmitter } from 'events'; +import { createTestConfig } from '../../../test/config/test-config'; +import createStores from '../../../test/fixtures/store'; +import permissions from '../../../test/fixtures/permissions'; +import getLogger from '../../../test/fixtures/no-logger'; +import getApp from '../../app'; +import { createServices } from '../../services'; const eventBus = new EventEmitter(); let request; @@ -15,10 +13,10 @@ let destroy; let strategyStore; let base; -function getSetup(databaseIsUp = true) { +function getSetup() { const randomBase = `/random${Math.round(Math.random() * 1000)}`; const perms = permissions(); - const stores = store.createStores(databaseIsUp); + const stores = createStores(); const config = createTestConfig({ server: { baseUriPath: randomBase }, preRouterHook: perms.hook, @@ -54,13 +52,13 @@ afterEach(() => { getLogger.setMuteError(false); }); -test('add version numbers for /stategies', () => { +test('add version numbers for /strategies', () => { expect.assertions(1); return request .get(`${base}/api/admin/strategies`) .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.version).toBe(1); }); }); @@ -71,7 +69,7 @@ test('require a name when creating a new strategy', () => { .post(`${base}/api/admin/strategies`) .send({}) .expect(400) - .expect(res => { + .expect((res) => { expect(res.body.details[0].message === '"name" is required').toBe( true, ); @@ -85,7 +83,7 @@ test('require parameters array when creating a new stratey', () => { .post(`${base}/api/admin/strategies`) .send({ name: 'TestStrat' }) .expect(400) - .expect(res => { + .expect((res) => { expect(res.body.details[0].message).toEqual( '"parameters" is required', ); @@ -193,7 +191,7 @@ test('deprecating a strategy works', async () => { return request .get(`${base}/api/admin/strategies/${name}`) .expect(200) - .expect(res => expect(res.body.deprecated).toBe(true)); + .expect((res) => expect(res.body.deprecated).toBe(true)); }); test('deprecating a non-existent strategy yields 404', () => { @@ -217,7 +215,7 @@ test('reactivating a strategy works', async () => { return request .get(`${base}/api/admin/strategies/${name}`) .expect(200) - .expect(res => expect(res.body.deprecated).toBe(false)); + .expect((res) => expect(res.body.deprecated).toBe(false)); }); test('reactivating a non-existent strategy yields 404', () => { @@ -234,11 +232,3 @@ test("deprecating 'default' strategy will yield 403", () => { .set('Content-Type', 'application/json') .expect(403); }); - -test('Getting strategies while database is down should yield 500', async () => { - expect.assertions(0); - // eslint-disable-next-line @typescript-eslint/no-shadow - const { request, base, destroy } = getSetup(false); - await request.get(`${base}/api/admin/strategies`).expect(500); - destroy(); -}); diff --git a/src/lib/routes/admin-api/strategy.ts b/src/lib/routes/admin-api/strategy.ts index b56b9cf0cc..a97637d86b 100644 --- a/src/lib/routes/admin-api/strategy.ts +++ b/src/lib/routes/admin-api/strategy.ts @@ -1,5 +1,3 @@ -'use strict'; - import { IUnleashConfig } from '../../types/option'; import { IUnleashServices } from '../../types/services'; import StrategyService from '../../services/strategy-service'; diff --git a/src/lib/routes/admin-api/tag-type.ts b/src/lib/routes/admin-api/tag-type.ts index c7ab855404..0d4b10c182 100644 --- a/src/lib/routes/admin-api/tag-type.ts +++ b/src/lib/routes/admin-api/tag-type.ts @@ -1,5 +1,3 @@ -'use strict'; - import { Request, Response } from 'express'; import Controller from '../controller'; diff --git a/src/lib/routes/admin-api/tag.test.js b/src/lib/routes/admin-api/tag.test.ts similarity index 75% rename from src/lib/routes/admin-api/tag.test.js rename to src/lib/routes/admin-api/tag.test.ts index 80569aab2c..afd19d7e83 100644 --- a/src/lib/routes/admin-api/tag.test.js +++ b/src/lib/routes/admin-api/tag.test.ts @@ -1,19 +1,16 @@ -'use strict'; - -const supertest = require('supertest'); -const { EventEmitter } = require('events'); -const store = require('../../../test/fixtures/store'); -const permissions = require('../../../test/fixtures/permissions'); -const getLogger = require('../../../test/fixtures/no-logger'); -const getApp = require('../../app'); -const { createTestConfig } = require('../../../test/config/test-config'); -const { createServices } = require('../../services'); +import supertest from 'supertest'; +import { EventEmitter } from 'events'; +import createStores from '../../../test/fixtures/store'; +import permissions from '../../../test/fixtures/permissions'; +import getApp from '../../app'; +import { createTestConfig } from '../../../test/config/test-config'; +import { createServices } from '../../services'; const eventBus = new EventEmitter(); -function getSetup(databaseIsUp = true) { +function getSetup() { const base = `/random${Math.round(Math.random() * 1000)}`; - const stores = store.createStores(databaseIsUp); + const stores = createStores(); const perms = permissions(); const config = createTestConfig({ server: { baseUriPath: base }, @@ -57,7 +54,7 @@ test('should get empty getTags via admin', () => { .get(`${base}/api/admin/tags`) .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.tags.length === 0).toBe(true); }); }); @@ -73,7 +70,7 @@ test('should get all tags added', () => { .get(`${base}/api/admin/tags`) .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.tags.length === 1).toBe(true); }); }); @@ -85,13 +82,13 @@ test('should be able to get single tag by type and value', () => { .get(`${base}/api/admin/tags/simple/TeamRed`) .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.tag.value).toBe('TeamRed'); }); }); test('trying to get non-existing tag by name and type should not be found', () => - request.get(`${base}/api/admin/tags/simple/TeamRed`).expect(res => { + request.get(`${base}/api/admin/tags/simple/TeamRed`).expect((res) => { expect(res.status).toBe(404); })); test('should be able to delete a tag', () => { @@ -108,7 +105,7 @@ test('should get empty tags of type', () => { .get(`${base}/api/admin/tags/simple`) .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.tags.length).toBe(0); }); }); @@ -120,17 +117,8 @@ test('should be able to filter by type', () => { .get(`${base}/api/admin/tags/simple`) .expect(200) .expect('Content-Type', /json/) - .expect(res => { + .expect((res) => { expect(res.body.tags.length).toBe(1); expect(res.body.tags[0].value).toBe('TeamRed'); }); }); - -test('Getting tags while database is down should be a 500', async () => { - expect.assertions(0); - getLogger.setMuteError(true); - // eslint-disable-next-line @typescript-eslint/no-shadow - const { request, base } = getSetup(false); - await request.get(`${base}/api/admin/tags`).expect(500); - destroy(); -}); diff --git a/src/lib/routes/admin-api/tag.ts b/src/lib/routes/admin-api/tag.ts index 1d0195c25c..ceff7134cb 100644 --- a/src/lib/routes/admin-api/tag.ts +++ b/src/lib/routes/admin-api/tag.ts @@ -1,5 +1,3 @@ -'use strict'; - import { Request, Response } from 'express'; import { IUnleashConfig } from '../../types/option'; import { IUnleashServices } from '../../types/services'; diff --git a/src/lib/routes/admin-api/user-admin.ts b/src/lib/routes/admin-api/user-admin.ts index 067f61f70d..816b90a21c 100644 --- a/src/lib/routes/admin-api/user-admin.ts +++ b/src/lib/routes/admin-api/user-admin.ts @@ -10,6 +10,7 @@ import { EmailService } from '../../services/email-service'; import ResetTokenService from '../../services/reset-token-service'; import { IUnleashServices } from '../../types/services'; import SessionService from '../../services/session-service'; +import { IAuthRequest } from '../unleash-types'; export default class UserAdminController extends Controller { private userService: UserService; @@ -65,10 +66,8 @@ export default class UserAdminController extends Controller { const { user } = req; try { const receiver = req.body.id; - const resetPasswordUrl = await this.userService.createResetPasswordEmail( - receiver, - user, - ); + const resetPasswordUrl = + await this.userService.createResetPasswordEmail(receiver, user); res.json({ resetPasswordUrl }); } catch (e) { handleErrors(res, this.logger, e); @@ -76,13 +75,14 @@ export default class UserAdminController extends Controller { } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - async getUsers(req, res): Promise { + async getUsers(req: Request, res: Response): Promise { try { const users = await this.userService.getAll(); const rootRoles = await this.accessService.getRootRoles(); - const inviteLinks = await this.resetTokenService.getActiveInvitations(); + const inviteLinks = + await this.resetTokenService.getActiveInvitations(); - const usersWithInviteLinks = users.map(user => { + const usersWithInviteLinks = users.map((user) => { const inviteLink = inviteLinks[user.id] || ''; return { ...user, inviteLink }; }); @@ -104,8 +104,8 @@ export default class UserAdminController extends Controller { } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - async search(req, res): Promise { - const { q } = req.query; + async search(req: Request, res: Response): Promise { + const { q } = req.query as any; try { const users = q && q.length > 1 ? await this.userService.search(q) : []; @@ -117,7 +117,7 @@ export default class UserAdminController extends Controller { } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - async createUser(req, res): Promise { + async createUser(req: IAuthRequest, res: Response): Promise { const { username, email, name, rootRole } = req.body; const { user } = req; @@ -171,8 +171,7 @@ export default class UserAdminController extends Controller { } } - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - async updateUser(req, res): Promise { + async updateUser(req: IAuthRequest, res: Response): Promise { const { user, params, body } = req; const { id } = params; @@ -195,8 +194,7 @@ export default class UserAdminController extends Controller { } } - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - async deleteUser(req, res): Promise { + async deleteUser(req: IAuthRequest, res: Response): Promise { const { user, params } = req; const { id } = params; @@ -208,8 +206,7 @@ export default class UserAdminController extends Controller { } } - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - async validatePassword(req, res): Promise { + async validatePassword(req: IAuthRequest, res: Response): Promise { const { password } = req.body; try { @@ -220,8 +217,7 @@ export default class UserAdminController extends Controller { } } - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - async changePassword(req, res): Promise { + async changePassword(req: IAuthRequest, res: Response): Promise { const { id } = req.params; const { password } = req.body; diff --git a/src/lib/routes/admin-api/user.test.js b/src/lib/routes/admin-api/user.test.ts similarity index 54% rename from src/lib/routes/admin-api/user.test.js rename to src/lib/routes/admin-api/user.test.ts index 792975229e..0622618e52 100644 --- a/src/lib/routes/admin-api/user.test.js +++ b/src/lib/routes/admin-api/user.test.ts @@ -1,25 +1,23 @@ -'use strict'; +import supertest from 'supertest'; +import { EventEmitter } from 'events'; +import { createServices } from '../../services'; +import { createTestConfig } from '../../../test/config/test-config'; -const supertest = require('supertest'); -const { EventEmitter } = require('events'); -const { createServices } = require('../../services'); -const { createTestConfig } = require('../../../test/config/test-config'); - -const store = require('../../../test/fixtures/store'); -const getApp = require('../../app'); -const User = require('../../types/user'); +import createStores from '../../../test/fixtures/store'; +import getApp from '../../app'; +import User from '../../types/user'; const eventBus = new EventEmitter(); const currentUser = new User({ id: 1337, email: 'test@mail.com' }); -function getSetup() { +async function getSetup() { const base = `/random${Math.round(Math.random() * 1000)}`; - const stores = store.createStores(); - stores.userStore.insert(currentUser); + const stores = createStores(); + await stores.userStore.insert(currentUser); const config = createTestConfig({ - preHook: a => { + preHook: (a) => { a.use((req, res, next) => { req.user = currentUser; next(); @@ -32,19 +30,20 @@ function getSetup() { return { base, userStore: stores.userStore, + sessionStore: stores.sessionStore, request: supertest(app), }; } -test('should return current user', () => { +test('should return current user', async () => { expect.assertions(1); - const { request, base } = getSetup(); + const { request, base } = await getSetup(); return request .get(`${base}/api/admin/user`) .expect(200) .expect('Content-Type', /json/) - .expect(res => { + .expect((res) => { expect(res.body.user.email).toBe(currentUser.email); }); }); @@ -52,20 +51,41 @@ const owaspPassword = 't7GTx&$Y9pcsnxRv6'; test('should allow user to change password', async () => { expect.assertions(2); - const { request, base, userStore } = getSetup(); - const before = await userStore.get(currentUser); + const { request, base, userStore } = await getSetup(); + const before = await userStore.get(currentUser.id); + // @ts-ignore expect(before.passwordHash).toBeFalsy(); await request .post(`${base}/api/admin/user/change-password`) .send({ password: owaspPassword, confirmPassword: owaspPassword }) .expect(200); - const updated = await userStore.get(currentUser); + const updated = await userStore.get(currentUser.id); + // @ts-ignore expect(updated.passwordHash).toBeTruthy(); }); +test('should get my sessions', async () => { + const { request, base, sessionStore } = await getSetup(); + + sessionStore.insertSession({ + sid: '123', + sess: { user: currentUser }, + }); + + await request + .get(`${base}/api/admin/user/my-sessions`) + .expect(200) + .expect((res) => { + expect(res.body.length).toBe(1); + expect(res.body[0].sid).toBe('123'); + expect(res.body[0].sess.user.id).toBe(currentUser.id); + expect(res.body[0].sess.user.email).toBe(currentUser.email); + }); +}); + test('should deny if password and confirmPassword are not equal', async () => { expect.assertions(0); - const { request, base } = getSetup(); + const { request, base } = await getSetup(); return request .post(`${base}/api/admin/user/change-password`) .send({ password: owaspPassword, confirmPassword: 'somethingelse' }) @@ -74,7 +94,7 @@ test('should deny if password and confirmPassword are not equal', async () => { test('should deny if password does not fulfill owasp criteria', async () => { expect.assertions(0); - const { request, base } = getSetup(); + const { request, base } = await getSetup(); return request .post(`${base}/api/admin/user/change-password`) .send({ password: 'hunter123', confirmPassword: 'hunter123' }) diff --git a/src/lib/routes/admin-api/user.ts b/src/lib/routes/admin-api/user.ts index b8c9e4655d..5dcbf9c7a2 100644 --- a/src/lib/routes/admin-api/user.ts +++ b/src/lib/routes/admin-api/user.ts @@ -1,5 +1,3 @@ -'use strict'; - import { Request, Response } from 'express'; import { IAuthRequest } from '../unleash-types'; import Controller from '../controller'; @@ -64,21 +62,17 @@ class UserController extends Controller { async getUser(req: IAuthRequest, res: Response): Promise { res.setHeader('cache-control', 'no-store'); const { user } = req; - if (user) { - const permissions = await this.accessService.getPermissionsForUser( - user, - ); - const feedback = await this.userFeedbackService.getAllUserFeedback( - user, - ); + const permissions = await this.accessService.getPermissionsForUser( + user, + ); + const feedback = await this.userFeedbackService.getAllUserFeedback( + user, + ); - delete user.permissions; // TODO: remove - return res - .status(200) - .json({ user, permissions, feedback }) - .end(); - } - return res.status(404).end(); + // TODO: remove this line after we remove it from db. + delete user.permissions; + + return res.status(200).json({ user, permissions, feedback }).end(); } async updateUserPass( @@ -86,40 +80,29 @@ class UserController extends Controller { res: Response, ): Promise { const { user } = req; - if (user) { - const { password, confirmPassword } = req.body; - try { - if (password === confirmPassword) { - this.userService.validatePassword(password); - await this.userService.changePassword(user.id, password); - res.status(200).end(); - } else { - res.status(400).end(); - } - } catch (e) { - handleErrors(res, this.logger, e); + const { password, confirmPassword } = req.body; + try { + if (password === confirmPassword) { + this.userService.validatePassword(password); + await this.userService.changePassword(user.id, password); + res.status(200).end(); + } else { + res.status(400).end(); } - } else { - res.status(401).end(); + } catch (e) { + handleErrors(res, this.logger, e); } } - async mySessions( - req: IUserRequest, - res: Response, - ): Promise { + async mySessions(req: IAuthRequest, res: Response): Promise { const { user } = req; - if (user) { - try { - const sessions = await this.sessionService.getSessionsForUser( - user.id, - ); - res.json(sessions); - } catch (e) { - handleErrors(res, this.logger, e); - } - } else { - res.status(401).end(); + try { + const sessions = await this.sessionService.getSessionsForUser( + user.id, + ); + res.json(sessions); + } catch (e) { + handleErrors(res, this.logger, e); } } } diff --git a/src/lib/routes/admin-api/util.js b/src/lib/routes/admin-api/util.js deleted file mode 100644 index c13e15b48e..0000000000 --- a/src/lib/routes/admin-api/util.js +++ /dev/null @@ -1,89 +0,0 @@ -'use strict'; - -const joi = require('joi'); - -const customJoi = joi.extend(j => ({ - type: 'isUrlFriendly', - base: j.string(), - messages: { - 'isUrlFriendly.base': '{{#label}} must be URL friendly', - }, - validate(value, helpers) { - // Base validation regardless of the rules applied - if (encodeURIComponent(value) !== value) { - // Generate an error, state and options need to be passed - return { value, errors: helpers.error('isUrlFriendly.base') }; - } - return undefined; - }, -})); - -const nameType = customJoi - .isUrlFriendly() - .min(1) - .max(100) - .required(); - -const handleErrors = (res, logger, error) => { - logger.warn(error.message); - // eslint-disable-next-line no-param-reassign - error.isJoi = true; - switch (error.name) { - case 'NoAccessError': - return res - .status(403) - .json(error) - .end(); - case 'NotFoundError': - return res - .status(404) - .json(error) - .end(); - case 'InvalidOperationError': - case 'NameExistsError': - return res - .status(409) - .json(error) - .end(); - case 'ValidationError': - return res - .status(400) - .json(error) - .end(); - case 'BadDataError': - return res - .status(400) - .json(error) - .end(); - case 'FeatureHasTagError': - return res - .status(409) - .json(error) - .end(); - case 'UsedTokenError': - return res - .status(403) - .json(error) - .end(); - case 'InvalidTokenError': - return res - .status(401) - .json(error) - .end(); - case 'OwaspValidationError': - return res - .status(400) - .json(error) - .end(); - case 'PasswordUndefinedError': - return res - .status(400) - .json(error) - .end(); - default: - logger.error('Server failed executing request', error); - return res.status(500).end(); - } -}; - -module.exports = { customJoi, nameType, handleErrors }; diff --git a/src/lib/routes/admin-api/util.ts b/src/lib/routes/admin-api/util.ts new file mode 100644 index 0000000000..df68845815 --- /dev/null +++ b/src/lib/routes/admin-api/util.ts @@ -0,0 +1,58 @@ +import joi from 'joi'; +import { Response } from 'express'; +import { Logger } from '../../logger'; + +export const customJoi = joi.extend((j) => ({ + type: 'isUrlFriendly', + base: j.string(), + messages: { + 'isUrlFriendly.base': '{{#label}} must be URL friendly', + }, + validate(value, helpers) { + // Base validation regardless of the rules applied + if (encodeURIComponent(value) !== value) { + // Generate an error, state and options need to be passed + return { value, errors: helpers.error('isUrlFriendly.base') }; + } + return undefined; + }, +})); + +export const nameType = customJoi.isUrlFriendly().min(1).max(100).required(); + +export const handleErrors: ( + res: Response, + logger: Logger, + error: Error, +) => void = (res, logger, error) => { + logger.warn(error.message); + // @ts-ignore + // eslint-disable-next-line no-param-reassign + error.isJoi = true; + switch (error.name) { + case 'NoAccessError': + return res.status(403).json(error).end(); + case 'NotFoundError': + return res.status(404).json(error).end(); + case 'InvalidOperationError': + case 'NameExistsError': + return res.status(409).json(error).end(); + case 'ValidationError': + return res.status(400).json(error).end(); + case 'BadDataError': + return res.status(400).json(error).end(); + case 'FeatureHasTagError': + return res.status(409).json(error).end(); + case 'UsedTokenError': + return res.status(403).json(error).end(); + case 'InvalidTokenError': + return res.status(401).json(error).end(); + case 'OwaspValidationError': + return res.status(400).json(error).end(); + case 'PasswordUndefinedError': + return res.status(400).json(error).end(); + default: + logger.error('Server failed executing request', error); + return res.status(500).end(); + } +}; diff --git a/src/lib/routes/auth/reset-password-controller.ts b/src/lib/routes/auth/reset-password-controller.ts index d3f6e9b87b..07604bba19 100644 --- a/src/lib/routes/auth/reset-password-controller.ts +++ b/src/lib/routes/auth/reset-password-controller.ts @@ -17,7 +17,6 @@ interface IChangePasswordBody { interface SessionRequest extends Request { - session?; user?; } @@ -90,7 +89,7 @@ class ResetPasswordController extends Controller { private async logout(req: SessionRequest) { if (req.session) { - req.session.destroy(); + req.session.destroy(() => {}); } } } diff --git a/src/lib/routes/backstage.test.js b/src/lib/routes/backstage.test.ts similarity index 61% rename from src/lib/routes/backstage.test.js rename to src/lib/routes/backstage.test.ts index 971d41784d..a47e938063 100644 --- a/src/lib/routes/backstage.test.js +++ b/src/lib/routes/backstage.test.ts @@ -1,18 +1,16 @@ -'use strict'; +import supertest from 'supertest'; +import { EventEmitter } from 'events'; +import { createServices } from '../services'; +import { createTestConfig } from '../../test/config/test-config'; -const supertest = require('supertest'); -const { EventEmitter } = require('events'); -const { createServices } = require('../services'); -const { createTestConfig } = require('../../test/config/test-config'); - -const store = require('../../test/fixtures/store'); -const getApp = require('../app'); +import createStores from '../../test/fixtures/store'; +import getApp from '../app'; const eventBus = new EventEmitter(); test('should enable prometheus', async () => { expect.assertions(0); - const stores = store.createStores(); + const stores = createStores(); const config = createTestConfig(); const services = createServices(stores, config); diff --git a/src/lib/routes/client-api/feature.test.js b/src/lib/routes/client-api/feature.test.ts similarity index 87% rename from src/lib/routes/client-api/feature.test.js rename to src/lib/routes/client-api/feature.test.ts index 5ca07f04c7..e4daa7291f 100644 --- a/src/lib/routes/client-api/feature.test.js +++ b/src/lib/routes/client-api/feature.test.ts @@ -1,19 +1,17 @@ -'use strict'; - -const supertest = require('supertest'); -const { EventEmitter } = require('events'); -const store = require('../../../test/fixtures/store'); -const getLogger = require('../../../test/fixtures/no-logger'); -const getApp = require('../../app'); -const { createServices } = require('../../services'); -const FeatureController = require('./feature'); -const { createTestConfig } = require('../../../test/config/test-config'); +import supertest from 'supertest'; +import { EventEmitter } from 'events'; +import createStores from '../../../test/fixtures/store'; +import getLogger from '../../../test/fixtures/no-logger'; +import getApp from '../../app'; +import { createServices } from '../../services'; +import FeatureController from './feature'; +import { createTestConfig } from '../../../test/config/test-config'; const eventBus = new EventEmitter(); function getSetup() { const base = `/random${Math.round(Math.random() * 1000)}`; - const stores = store.createStores(); + const stores = createStores(); const config = createTestConfig({ server: { baseUriPath: base }, }); @@ -57,7 +55,7 @@ test('should get empty getFeatures via client', () => { .get(`${base}/api/client/features`) .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.features.length === 0).toBe(true); }); }); @@ -69,6 +67,7 @@ test('if caching is enabled should memoize', async () => { getClientFeatures, }; const controller = new FeatureController( + // @ts-ignore { featureToggleServiceV2 }, { getLogger, @@ -80,7 +79,9 @@ test('if caching is enabled should memoize', async () => { }, }, ); + // @ts-ignore await controller.getAll({ query: {} }, { json: () => {} }); + // @ts-ignore await controller.getAll({ query: {} }, { json: () => {} }); expect(getClientFeatures).toHaveBeenCalledTimes(1); }); @@ -92,6 +93,7 @@ test('if caching is not enabled all calls goes to service', async () => { getClientFeatures, }; const controller = new FeatureController( + // @ts-ignore { featureToggleServiceV2 }, { getLogger, @@ -103,7 +105,9 @@ test('if caching is not enabled all calls goes to service', async () => { }, }, ); + // @ts-ignore await controller.getAll({ query: {} }, { json: () => {} }); + // @ts-ignore await controller.getAll({ query: {} }, { json: () => {} }); expect(getClientFeatures).toHaveBeenCalledTimes(2); }); @@ -119,7 +123,7 @@ test('fetch single feature', async () => { .get(`${base}/api/client/features/test_`) .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.name === 'test_').toBe(true); }); }); @@ -137,7 +141,7 @@ test('support name prefix', async () => { .get(`${base}/api/client/features?namePrefix=${namePrefix}`) .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.features.length).toBe(2); expect(res.body.features[1].name).toBe('b_test2'); }); @@ -157,7 +161,7 @@ test('support filtering on project', async () => { .get(`${base}/api/client/features?project=projecta`) .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.features).toHaveLength(1); expect(res.body.features[0].name).toBe('a_test1'); }); diff --git a/src/lib/routes/client-api/feature.ts b/src/lib/routes/client-api/feature.ts index db7483dece..f7f239944e 100644 --- a/src/lib/routes/client-api/feature.ts +++ b/src/lib/routes/client-api/feature.ts @@ -9,16 +9,7 @@ import { Logger } from '../../logger'; import { querySchema } from '../../schema/feature-schema'; import { IFeatureToggleQuery } from '../../types/model'; -const version = 1; - -const FEATURE_COLUMNS_CLIENT = [ - 'name', - 'type', - 'enabled', - 'stale', - 'strategies', - 'variants', -]; +const version = 2; export default class FeatureController extends Controller { private readonly logger: Logger; @@ -45,7 +36,7 @@ export default class FeatureController extends Controller { // @ts-ignore this.cache = experimental.clientFeatureMemoize.enabled; this.cachedFeatures = memoizee( - query => this.featureToggleServiceV2.getClientFeatures(query), + (query) => this.featureToggleServiceV2.getClientFeatures(query), { promise: true, // @ts-ignore @@ -70,7 +61,7 @@ export default class FeatureController extends Controller { query, ); } - res.json({ version: 2, features }); + res.json({ version, features }); } catch (e) { handleErrors(res, this.logger, e); } @@ -92,7 +83,7 @@ export default class FeatureController extends Controller { namePrefix, }); if (query.tag) { - query.tag = query.tag.map(q => q.split(':')); + query.tag = query.tag.map((q) => q.split(':')); } return query; } diff --git a/src/lib/routes/client-api/index.js b/src/lib/routes/client-api/index.js deleted file mode 100644 index 137bed9188..0000000000 --- a/src/lib/routes/client-api/index.js +++ /dev/null @@ -1,29 +0,0 @@ -'use strict'; - -const Controller = require('../controller'); -const FeatureController = require('./feature'); -const MetricsController = require('./metrics'); -const RegisterController = require('./register'); -const apiDef = require('./api-def.json'); - -class ClientApi extends Controller { - constructor(config, services = {}) { - super(); - - const { getLogger } = config; - - this.get('/', this.index); - this.use('/features', new FeatureController(services, config).router); - this.use('/metrics', new MetricsController(services, getLogger).router); - this.use( - '/register', - new RegisterController(services, getLogger).router, - ); - } - - index(req, res) { - res.json(apiDef); - } -} - -module.exports = ClientApi; diff --git a/src/lib/routes/client-api/index.ts b/src/lib/routes/client-api/index.ts new file mode 100644 index 0000000000..a5532c330d --- /dev/null +++ b/src/lib/routes/client-api/index.ts @@ -0,0 +1,25 @@ +import { Request, Response } from 'express'; +import Controller from '../controller'; +import FeatureController from './feature'; +import MetricsController from './metrics'; +import RegisterController from './register'; +import { IUnleashConfig } from '../../types/option'; +import { IUnleashServices } from '../../types'; + +const apiDef = require('./api-def.json'); + +export default class ClientApi extends Controller { + constructor(config: IUnleashConfig, services: IUnleashServices) { + super(config); + this.get('/', this.index); + this.use('/features', new FeatureController(services, config).router); + this.use('/metrics', new MetricsController(services, config).router); + this.use('/register', new RegisterController(services, config).router); + } + + index(req: Request, res: Response): void { + res.json(apiDef); + } +} + +module.exports = ClientApi; diff --git a/src/lib/routes/client-api/metrics.js b/src/lib/routes/client-api/metrics.js deleted file mode 100644 index cfc691130c..0000000000 --- a/src/lib/routes/client-api/metrics.js +++ /dev/null @@ -1,33 +0,0 @@ -'use strict'; - -const Controller = require('../controller'); - -class ClientMetricsController extends Controller { - constructor({ clientMetricsService }, getLogger) { - super(); - this.logger = getLogger('/api/client/metrics'); - this.metrics = clientMetricsService; - - this.post('/', this.registerMetrics); - } - - async registerMetrics(req, res) { - const data = req.body; - const clientIp = req.ip; - - try { - await this.metrics.registerClientMetrics(data, clientIp); - return res.status(202).end(); - } catch (e) { - this.logger.warn('Failed to store metrics', e); - switch (e.name) { - case 'ValidationError': - return res.status(400).end(); - default: - return res.status(500).end(); - } - } - } -} - -module.exports = ClientMetricsController; diff --git a/src/lib/routes/client-api/metrics.test.js b/src/lib/routes/client-api/metrics.test.ts similarity index 89% rename from src/lib/routes/client-api/metrics.test.js rename to src/lib/routes/client-api/metrics.test.ts index 300748c06a..e84e51d1cf 100644 --- a/src/lib/routes/client-api/metrics.test.js +++ b/src/lib/routes/client-api/metrics.test.ts @@ -1,19 +1,15 @@ -'use strict'; - -const supertest = require('supertest'); -const { EventEmitter } = require('events'); -const store = require('../../../test/fixtures/store'); -const getApp = require('../../app'); -const { createTestConfig } = require('../../../test/config/test-config'); -const { - clientMetricsSchema, -} = require('../../services/client-metrics/client-metrics-schema'); -const { createServices } = require('../../services'); +import supertest from 'supertest'; +import { EventEmitter } from 'events'; +import createStores from '../../../test/fixtures/store'; +import getApp from '../../app'; +import { createTestConfig } from '../../../test/config/test-config'; +import { clientMetricsSchema } from '../../services/client-metrics/client-metrics-schema'; +import { createServices } from '../../services'; const eventBus = new EventEmitter(); function getSetup() { - const stores = store.createStores(); + const stores = createStores(); const config = createTestConfig(); const services = createServices(stores, config); @@ -191,7 +187,7 @@ test('should set lastSeen on toggle', async () => { }) .expect(202); - const toggle = await stores.featureToggleStore.getFeature('toggleLastSeen'); + const toggle = await stores.featureToggleStore.get('toggleLastSeen'); expect(toggle.lastSeenAt).toBeTruthy(); }); diff --git a/src/lib/routes/client-api/metrics.ts b/src/lib/routes/client-api/metrics.ts new file mode 100644 index 0000000000..7105002a5f --- /dev/null +++ b/src/lib/routes/client-api/metrics.ts @@ -0,0 +1,48 @@ +import { Request, Response } from 'express'; +import Controller from '../controller'; +import { IUnleashServices } from '../../types'; +import { IUnleashConfig } from '../../types/option'; +import ClientMetricsService from '../../services/client-metrics'; +import { Logger } from '../../logger'; + +export default class ClientMetricsController extends Controller { + logger: Logger; + + metrics: ClientMetricsService; + + constructor( + { + clientMetricsService, + }: Pick, + config: IUnleashConfig, + ) { + super(config); + this.logger = config.getLogger('/api/client/metrics'); + this.metrics = clientMetricsService; + + this.post('/', this.registerMetrics); + } + + async registerMetrics( + req: Request, + res: Response, + ): Promise { + const data = req.body; + const clientIp = req.ip; + + try { + await this.metrics.registerClientMetrics(data, clientIp); + return res.status(202).end(); + } catch (e) { + this.logger.warn('Failed to store metrics', e); + switch (e.name) { + case 'ValidationError': + return res.status(400).end(); + default: + return res.status(500).end(); + } + } + } +} + +module.exports = ClientMetricsController; diff --git a/src/lib/routes/client-api/register.js b/src/lib/routes/client-api/register.js deleted file mode 100644 index fbced5d943..0000000000 --- a/src/lib/routes/client-api/register.js +++ /dev/null @@ -1,31 +0,0 @@ -'use strict'; - -const Controller = require('../controller'); - -class RegisterController extends Controller { - constructor({ clientMetricsService }, getLogger) { - super(); - this.logger = getLogger('/api/client/register'); - this.metrics = clientMetricsService; - this.post('/', this.handleRegister); - } - - async handleRegister(req, res) { - const data = req.body; - try { - const clientIp = req.ip; - await this.metrics.registerClient(data, clientIp); - return res.status(202).end(); - } catch (err) { - this.logger.warn('failed to register client', err); - switch (err.name) { - case 'ValidationError': - return res.status(400).end(); - default: - return res.status(500).end(); - } - } - } -} - -module.exports = RegisterController; diff --git a/src/lib/routes/client-api/register.test.js b/src/lib/routes/client-api/register.test.ts similarity index 82% rename from src/lib/routes/client-api/register.test.js rename to src/lib/routes/client-api/register.test.ts index 7ea9a288f9..f31786498d 100644 --- a/src/lib/routes/client-api/register.test.js +++ b/src/lib/routes/client-api/register.test.ts @@ -1,17 +1,15 @@ -'use strict'; - -const supertest = require('supertest'); -const { EventEmitter } = require('events'); -const { createTestConfig } = require('../../../test/config/test-config'); -const store = require('../../../test/fixtures/store'); -const getLogger = require('../../../test/fixtures/no-logger'); -const getApp = require('../../app'); -const { createServices } = require('../../services'); +import supertest from 'supertest'; +import { EventEmitter } from 'events'; +import { createTestConfig } from '../../../test/config/test-config'; +import createStores from '../../../test/fixtures/store'; +import getLogger from '../../../test/fixtures/no-logger'; +import getApp from '../../app'; +import { createServices } from '../../services'; const eventBus = new EventEmitter(); function getSetup() { - const stores = store.createStores(); + const stores = createStores(); const config = createTestConfig(); const services = createServices(stores, config); const app = getApp(config, stores, services, eventBus); diff --git a/src/lib/routes/client-api/register.ts b/src/lib/routes/client-api/register.ts new file mode 100644 index 0000000000..bd12e833fa --- /dev/null +++ b/src/lib/routes/client-api/register.ts @@ -0,0 +1,41 @@ +import { Request, Response } from 'express'; +import Controller from '../controller'; +import { IUnleashServices } from '../../types'; +import { IUnleashConfig } from '../../types/option'; +import { Logger } from '../../logger'; +import ClientMetricsService from '../../services/client-metrics'; + +export default class RegisterController extends Controller { + logger: Logger; + + metrics: ClientMetricsService; + + constructor( + { + clientMetricsService, + }: Pick, + config: IUnleashConfig, + ) { + super(config); + this.logger = config.getLogger('/api/client/register'); + this.metrics = clientMetricsService; + this.post('/', this.handleRegister); + } + + async handleRegister(req: Request, res: Response): Promise { + const data = req.body; + try { + const clientIp = req.ip; + await this.metrics.registerClient(data, clientIp); + return res.status(202).end(); + } catch (err) { + this.logger.warn('failed to register client', err); + switch (err.name) { + case 'ValidationError': + return res.status(400).end(); + default: + return res.status(500).end(); + } + } + } +} diff --git a/src/lib/routes/controller.ts b/src/lib/routes/controller.ts index 60e0f43437..cbb42a33c4 100644 --- a/src/lib/routes/controller.ts +++ b/src/lib/routes/controller.ts @@ -5,17 +5,14 @@ const { Router } = require('express'); const NoAccessError = require('../error/no-access-error'); const requireContentType = require('../middleware/content_type_checker'); -const checkPermission = permission => async (req, res, next) => { +const checkPermission = (permission) => async (req, res, next) => { if (!permission) { return next(); } if (req.checkRbac && (await req.checkRbac(permission))) { return next(); } - return res - .status(403) - .json(new NoAccessError(permission)) - .end(); + return res.status(403).json(new NoAccessError(permission)).end(); }; /** diff --git a/src/lib/routes/health-check.test.js b/src/lib/routes/health-check.test.ts similarity index 54% rename from src/lib/routes/health-check.test.js rename to src/lib/routes/health-check.test.ts index c2fa34ebf6..8e8c9ad226 100644 --- a/src/lib/routes/health-check.test.js +++ b/src/lib/routes/health-check.test.ts @@ -1,26 +1,24 @@ -'use strict'; +import supertest from 'supertest'; +import { EventEmitter } from 'events'; +import { createServices } from '../services'; +import { createTestConfig } from '../../test/config/test-config'; -const supertest = require('supertest'); -const { EventEmitter } = require('events'); -const { createServices } = require('../services'); -const { createTestConfig } = require('../../test/config/test-config'); - -const store = require('../../test/fixtures/store'); -const getLogger = require('../../test/fixtures/no-logger'); -const getApp = require('../app'); +import createStores from '../../test/fixtures/store'; +import getLogger from '../../test/fixtures/no-logger'; +import getApp from '../app'; +import { IUnleashStores } from '../types'; const eventBus = new EventEmitter(); function getSetup() { - const stores = store.createStores(); - const { db } = stores; + const stores = createStores(); const config = createTestConfig(); const services = createServices(stores, config); const app = getApp(config, stores, services, eventBus); return { - db, request: supertest(app), + stores, destroy: () => { services.versionService.destroy(); services.clientMetricsService.destroy(); @@ -29,13 +27,13 @@ function getSetup() { }; } let request; -let db; let destroy; +let stores; beforeEach(() => { const setup = getSetup(); request = setup.request; - db = setup.db; destroy = setup.destroy; + stores = setup.stores; }); afterEach(() => { @@ -44,13 +42,30 @@ afterEach(() => { }); test('should give 500 when db is failing', () => { + const config = createTestConfig(); + const failingStores: Partial = { + // @ts-ignore + featureTypeStore: { + getAll: () => Promise.reject(new Error('db error')), + }, + clientMetricsStore: { + // @ts-ignore + on: () => {}, + }, + }; + // @ts-ignore + const services = createServices(failingStores, config); + // @ts-ignore + const app = getApp(createTestConfig(), failingStores, services, eventBus); + request = supertest(app); getLogger.setMuteError(true); expect.assertions(2); - db.raw = () => Promise.reject(new Error('db error')); + stores.featureToggleStore.getAll = () => + Promise.reject(new Error('db error')); return request .get('/health') .expect(500) - .expect(res => { + .expect((res) => { expect(res.status).toBe(500); expect(res.body.health).toBe('BAD'); }); @@ -58,17 +73,15 @@ test('should give 500 when db is failing', () => { test('should give 200 when db is not failing', () => { expect.assertions(0); - db.raw = () => Promise.resolve(); return request.get('/health').expect(200); }); test('should give health=GOOD when db is not failing', () => { expect.assertions(2); - db.raw = () => Promise.resolve(); return request .get('/health') .expect(200) - .expect(res => { + .expect((res) => { expect(res.status).toBe(200); expect(res.body.health).toBe('GOOD'); }); diff --git a/src/lib/routes/health-check.ts b/src/lib/routes/health-check.ts index 1f4caf5f11..d8146d9358 100644 --- a/src/lib/routes/health-check.ts +++ b/src/lib/routes/health-check.ts @@ -1,5 +1,3 @@ -'use strict'; - import { Request, Response } from 'express'; import { IUnleashConfig } from '../types/option'; import { IUnleashServices } from '../types/services'; diff --git a/src/lib/routes/index.test.js b/src/lib/routes/index.test.ts similarity index 82% rename from src/lib/routes/index.test.js rename to src/lib/routes/index.test.ts index c9c4092be4..2c5fb8f662 100644 --- a/src/lib/routes/index.test.js +++ b/src/lib/routes/index.test.ts @@ -1,17 +1,15 @@ -'use strict'; - -const supertest = require('supertest'); -const { EventEmitter } = require('events'); -const { createTestConfig } = require('../../test/config/test-config'); -const store = require('../../test/fixtures/store'); -const getApp = require('../app'); -const { createServices } = require('../services'); +import supertest from 'supertest'; +import { EventEmitter } from 'events'; +import { createTestConfig } from '../../test/config/test-config'; +import createStores from '../../test/fixtures/store'; +import getApp from '../app'; +import { createServices } from '../services'; const eventBus = new EventEmitter(); function getSetup() { const base = `/random${Math.round(Math.random() * 1000)}`; - const stores = store.createStores(); + const stores = createStores(); const config = createTestConfig({ server: { baseUriPath: base }, }); @@ -43,13 +41,13 @@ afterEach(() => { destroy(); }); -test('api defintion', () => { +test('api definition', () => { expect.assertions(5); return request .get(`${base}/api/`) .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body).toBeTruthy(); const { admin, client } = res.body.links; expect(admin.uri === '/api/admin').toBe(true); @@ -69,7 +67,7 @@ test('admin api defintion', () => { .get(`${base}/api/admin`) .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body).toBeTruthy(); expect( res.body.links['feature-toggles'].uri === '/api/admin/features', @@ -83,7 +81,7 @@ test('client api defintion', () => { .get(`${base}/api/client`) .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body).toBeTruthy(); expect(res.body.links.metrics.uri === '/api/client/metrics').toBe( true, diff --git a/src/lib/routes/logout.test.ts b/src/lib/routes/logout.test.ts index 4b6a8576d3..c31c662afd 100644 --- a/src/lib/routes/logout.test.ts +++ b/src/lib/routes/logout.test.ts @@ -26,7 +26,7 @@ test('should redirect to "/basePath" after logout when baseUriPath is set', asyn const request = supertest(app); expect.assertions(0); await request - .get(`/logout`) + .get('/logout') .expect(302) .expect('Location', `${baseUriPath}/`); }); @@ -76,7 +76,7 @@ test('should redirect to alternative logoutUrl', async () => { app.use('/logout', new LogoutController(config).router); const request = supertest(app); await request - .get(`/logout`) + .get('/logout') .expect(302) .expect('Location', '/some-other-path'); }); diff --git a/src/lib/schema/feature-schema.ts b/src/lib/schema/feature-schema.ts index 754d25e412..dcf4787b96 100644 --- a/src/lib/schema/feature-schema.ts +++ b/src/lib/schema/feature-schema.ts @@ -9,39 +9,20 @@ export const nameSchema = joi export const constraintSchema = joi.object().keys({ contextName: joi.string(), operator: joi.string(), - values: joi - .array() - .items( - joi - .string() - .min(1) - .max(100), - ) - .min(1) - .optional(), + values: joi.array().items(joi.string().min(1).max(100)).min(1).optional(), }); export const strategiesSchema = joi.object().keys({ id: joi.string().optional(), name: nameType, - constraints: joi - .array() - .allow(null) - .items(constraintSchema), + constraints: joi.array().allow(null).items(constraintSchema), parameters: joi.object(), }); export const variantsSchema = joi.object().keys({ name: nameType, - weight: joi - .number() - .min(0) - .max(1000) - .required(), - weightType: joi - .string() - .valid('variable', 'fix') - .default('variable'), + weight: joi.number().min(0).max(1000).required(), + weightType: joi.string().valid('variable', 'fix').default('variable'), payload: joi .object() .keys({ @@ -68,21 +49,14 @@ export const featureMetadataSchema = joi stale: joi.boolean().default(false), archived: joi.boolean().default(false), type: joi.string().default('release'), - description: joi - .string() - .allow('') - .allow(null) - .optional(), + description: joi.string().allow('').allow(null).optional(), variants: joi .array() .allow(null) .unique((a, b) => a.name === b.name) .optional() .items(variantsSchema), - createdAt: joi - .date() - .optional() - .allow(null), + createdAt: joi.date().optional().allow(null), }) .options({ allowUnknown: false, stripUnknown: true, abortEarly: false }); @@ -95,11 +69,7 @@ export const featureSchema = joi archived: joi.boolean().default(false), type: joi.string().default('release'), project: joi.string().default('default'), - description: joi - .string() - .allow('') - .allow(null) - .optional(), + description: joi.string().allow('').allow(null).optional(), strategies: joi .array() .min(0) @@ -123,19 +93,9 @@ export const querySchema = joi .allow(null) .items(joi.string().pattern(/\w+:.+/, { name: 'tag' })) .optional(), - project: joi - .array() - .allow(null) - .items(nameType) - .optional(), - namePrefix: joi - .string() - .allow(null) - .optional(), - environment: joi - .string() - .allow(null) - .optional(), + project: joi.array().allow(null).items(nameType).optional(), + namePrefix: joi.string().allow(null).optional(), + environment: joi.string().allow(null).optional(), }) .options({ allowUnknown: false, stripUnknown: true, abortEarly: false }); diff --git a/src/lib/server-impl.test.js b/src/lib/server-impl.test.js index 31c51280b5..2c4f70938c 100644 --- a/src/lib/server-impl.test.js +++ b/src/lib/server-impl.test.js @@ -51,7 +51,7 @@ jest.mock('./db', () => ({ jest.mock( '../migrator', () => - function() { + function () { return Promise.resolve(); }, ); @@ -59,7 +59,7 @@ jest.mock( jest.mock( './util/version', () => - function() { + function () { return 'unleash-test-version'; }, ); diff --git a/src/lib/server-impl.ts b/src/lib/server-impl.ts index cf0c47bbda..77f3f0000e 100644 --- a/src/lib/server-impl.ts +++ b/src/lib/server-impl.ts @@ -17,6 +17,8 @@ import AuthenticationRequired from './types/authentication-required'; import * as eventType from './types/events'; import { addEventHook } from './event-hook'; import registerGracefulShutdown from './util/graceful-shutdown'; +import { createDb } from './db/db-pool'; +import sessionDb from './middleware/session-db'; async function createApp( config: IUnleashConfig, @@ -26,9 +28,11 @@ async function createApp( const logger = config.getLogger('server-impl.js'); const serverVersion = version; const eventBus = new EventEmitter(); - const stores = createStores(config, eventBus); + const db = createDb(config); + const stores = createStores(config, eventBus, db); const services = createServices(stores, config); const metricsMonitor = createMetricsMonitor(); + const unleashSession = sessionDb(config, db); const stopUnleash = async (server?: StoppableServer) => { logger.info('Shutting down Unleash...'); @@ -39,7 +43,7 @@ async function createApp( metricsMonitor.stopMonitoring(); stores.clientInstanceStore.destroy(); stores.clientMetricsStore.destroy(); - await stores.db.destroy(); + await db.destroy(); }; if (!config.server.secret) { @@ -47,12 +51,12 @@ async function createApp( // eslint-disable-next-line no-param-reassign config.server.secret = secret; } - const app = getApp(config, stores, services, eventBus); + const app = getApp(config, stores, services, eventBus, unleashSession); if (typeof config.eventHook === 'function') { addEventHook(config.eventHook, stores.eventStore); } - metricsMonitor.startMonitoring(config, stores, serverVersion, eventBus); + metricsMonitor.startMonitoring(config, stores, serverVersion, eventBus, db); const unleash: Omit = { stores, eventBus, diff --git a/src/lib/services/access-service.ts b/src/lib/services/access-service.ts index b07e02cd11..177f56d898 100644 --- a/src/lib/services/access-service.ts +++ b/src/lib/services/access-service.ts @@ -1,19 +1,28 @@ +import * as permissions from '../types/permissions'; +import User, { IUser } from '../types/user'; import { - AccessStore, + IAccessStore, IRole, IUserPermission, IUserRole, -} from '../db/access-store'; -import * as permissions from '../types/permissions'; -import User from '../types/user'; +} from '../types/stores/access-store'; +import { IUserStore } from '../types/stores/user-store'; +import { Logger } from '../logger'; +import { IUnleashStores } from '../types/stores'; +import { + IPermission, + IRoleData, + IUserWithRole, + PermissionType, + RoleName, + RoleType, +} from '../types/model'; export const ALL_PROJECTS = '*'; const PROJECT_DESCRIPTION = { - OWNER: - 'Users with this role have full control over the project, and can add and manage other users within the project context, manage feature toggles within the project, and control advanced project features like archiving and deleting the project.', - MEMBER: - 'Users with this role within a project are allowed to view, create and update feature toggles, but have limited permissions in regards to managing the projects user access and can not archive or delete the project.', + OWNER: 'Users with this role have full control over the project, and can add and manage other users within the project context, manage feature toggles within the project, and control advanced project features like archiving and deleting the project.', + MEMBER: 'Users with this role within a project are allowed to view, create and update feature toggles, but have limited permissions in regards to managing the projects user access and can not archive or delete the project.', }; const { ADMIN } = permissions; @@ -32,76 +41,28 @@ const PROJECT_REGULAR = [ permissions.DELETE_FEATURE, ]; -const isProjectPermission = permission => PROJECT_ADMIN.includes(permission); - -interface IStores { - accessStore: AccessStore; - userStore: any; -} - -export interface IUserWithRole { - id: number; - roleId: number; - name?: string; - username?: string; - email?: string; - imageUrl?: string; -} - -export interface IRoleData { - role: IRole; - users: User[]; - permissions: IUserPermission[]; -} - -export interface IPermission { - name: string; - type: PermissionType; -} - -enum PermissionType { - root = 'root', - project = 'project', -} - -export enum RoleName { - // eslint-disable-next-line @typescript-eslint/no-shadow - ADMIN = 'Admin', - EDITOR = 'Editor', - VIEWER = 'Viewer', - OWNER = 'Owner', - MEMBER = 'Member', -} - -export enum RoleType { - ROOT = 'root', - PROJECT = 'project', -} - -export interface IRoleIdentifier { - roleId?: number; - roleName?: RoleName; -} +const isProjectPermission = (permission) => PROJECT_ADMIN.includes(permission); export class AccessService { - public RoleName = RoleName; + private store: IAccessStore; - private store: AccessStore; + private userStore: IUserStore; - private userStore: any; - - private logger: any; + private logger: Logger; private permissions: IPermission[]; constructor( - { accessStore, userStore }: IStores, + { + accessStore, + userStore, + }: Pick, { getLogger }: { getLogger: Function }, ) { this.store = accessStore; this.userStore = userStore; this.logger = getLogger('/services/access-service.ts'); - this.permissions = Object.values(permissions).map(p => ({ + this.permissions = Object.values(permissions).map((p) => ({ name: p, type: isProjectPermission(p) ? PermissionType.project @@ -130,13 +91,14 @@ export class AccessService { return userP .filter( - p => + (p) => !p.project || p.project === projectId || p.project === ALL_PROJECTS, ) .some( - p => p.permission === permission || p.permission === ADMIN, + (p) => + p.permission === permission || p.permission === ADMIN, ); } catch (e) { this.logger.error( @@ -187,7 +149,7 @@ export class AccessService { async getUserRootRoles(userId: number): Promise { const userRoles = await this.store.getRolesForUserId(userId); - return userRoles.filter(r => r.type === RoleType.ROOT); + return userRoles.filter((r) => r.type === RoleType.ROOT); } async removeUserFromRole(userId: number, roleId: number): Promise { @@ -230,7 +192,7 @@ export class AccessService { async getRole(roleId: number): Promise { const [role, rolePerms, users] = await Promise.all([ - this.store.getRoleWithId(roleId), + this.store.get(roleId), this.store.getPermissionsForRole(roleId), this.getUsersForRole(roleId), ]); @@ -245,9 +207,12 @@ export class AccessService { return this.store.getRolesForUserId(userId); } - async getUsersForRole(roleId: number): Promise { + async getUsersForRole(roleId: number): Promise { const userIdList = await this.store.getUserIdsForRole(roleId); - return this.userStore.getAllWithId(userIdList); + if (userIdList.length > 0) { + return this.userStore.getAllWithId(userIdList); + } + return []; } // Move to project-service? @@ -257,9 +222,9 @@ export class AccessService { const roles = await this.store.getRolesForProject(projectId); const users = await Promise.all( - roles.map(async role => { + roles.map(async (role) => { const usrs = await this.getUsersForRole(role.id); - return usrs.map(u => ({ ...u, roleId: role.id })); + return usrs.map((u) => ({ ...u, roleId: role.id })); }), ); return [roles, users.flat()]; @@ -325,15 +290,15 @@ export class AccessService { const rootRoles = await this.getRootRoles(); let role: IRole; if (typeof rootRole === 'number') { - role = rootRoles.find(r => r.id === rootRole); + role = rootRoles.find((r) => r.id === rootRole); } else { - role = rootRoles.find(r => r.name === rootRole); + role = rootRoles.find((r) => r.name === rootRole); } return role; } async getRootRole(roleName: RoleName): Promise { const roles = await this.store.getRootRoles(); - return roles.find(r => r.name === roleName); + return roles.find((r) => r.name === roleName); } } diff --git a/src/lib/services/addon-schema.js b/src/lib/services/addon-schema.js deleted file mode 100644 index d70268c86d..0000000000 --- a/src/lib/services/addon-schema.js +++ /dev/null @@ -1,27 +0,0 @@ -const joi = require('joi'); -const { nameType } = require('../routes/admin-api/util'); - -const addonSchema = joi - .object() - .keys({ - provider: nameType, - enabled: joi.bool().default(true), - description: joi - .string() - .allow(null) - .allow('') - .optional(), - parameters: joi - .object() - .pattern(joi.string(), [joi.string(), joi.number(), joi.boolean()]) - .optional(), - events: joi - .array() - .optional() - .items(joi.string()), - }) - .options({ allowUnknown: false, stripUnknown: true }); - -module.exports = { - addonSchema, -}; diff --git a/src/lib/services/addon-schema.ts b/src/lib/services/addon-schema.ts new file mode 100644 index 0000000000..d642eeeb23 --- /dev/null +++ b/src/lib/services/addon-schema.ts @@ -0,0 +1,16 @@ +import joi from 'joi'; +import { nameType } from '../routes/admin-api/util'; + +export const addonSchema = joi + .object() + .keys({ + provider: nameType, + enabled: joi.bool().default(true), + description: joi.string().allow(null).allow('').optional(), + parameters: joi + .object() + .pattern(joi.string(), [joi.string(), joi.number(), joi.boolean()]) + .optional(), + events: joi.array().optional().items(joi.string()), + }) + .options({ allowUnknown: false, stripUnknown: true }); diff --git a/src/lib/services/addon-service-test-simple-addon.ts b/src/lib/services/addon-service-test-simple-addon.ts new file mode 100644 index 0000000000..634564b963 --- /dev/null +++ b/src/lib/services/addon-service-test-simple-addon.ts @@ -0,0 +1,74 @@ +import Addon from '../addons/addon'; +import getLogger from '../../test/fixtures/no-logger'; +import { IAddonDefinition, IEvent } from '../types/model'; +import { + FEATURE_ARCHIVED, + FEATURE_CREATED, + FEATURE_REVIVED, + FEATURE_UPDATED, +} from '../types/events'; + +const definition: IAddonDefinition = { + name: 'simple', + displayName: 'Simple ADdon', + description: 'Some description', + parameters: [ + { + name: 'url', + displayName: 'Some URL', + type: 'url', + required: true, + sensitive: false, + }, + { + name: 'var', + displayName: 'Some var', + description: 'Some variable to inject', + type: 'text', + required: false, + sensitive: false, + }, + { + name: 'sensitiveParam', + displayName: 'Some sensitive param', + description: 'Some variable to inject', + type: 'text', + required: false, + sensitive: true, + }, + ], + documentationUrl: 'https:/www.example.com', + events: [ + FEATURE_CREATED, + FEATURE_UPDATED, + FEATURE_ARCHIVED, + FEATURE_REVIVED, + ], + tagTypes: [ + { + name: 'me', + description: 'Some tag', + icon: 'm', + }, + ], +}; +export default class SimpleAddon extends Addon { + events: any[]; + + constructor() { + super(definition, { getLogger }); + this.events = []; + } + + getEvents(): any[] { + return this.events; + } + + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + async handleEvent(event: IEvent, parameters: any): Promise { + this.events.push({ + event, + parameters, + }); + } +} diff --git a/src/lib/services/addon-service.test.js b/src/lib/services/addon-service.test.ts similarity index 79% rename from src/lib/services/addon-service.test.js rename to src/lib/services/addon-service.test.ts index 1c13effcff..4e7fe2ed43 100644 --- a/src/lib/services/addon-service.test.js +++ b/src/lib/services/addon-service.test.ts @@ -1,95 +1,37 @@ -'use strict'; +import { ValidationError } from 'joi'; -const { ValidationError } = require('joi'); -const Addon = require('../addons/addon'); - -const store = require('../../test/fixtures/store'); -const getLogger = require('../../test/fixtures/no-logger'); -const TagTypeService = require('./tag-type-service'); -const { - FEATURE_CREATED, - FEATURE_UPDATED, - FEATURE_ARCHIVED, - FEATURE_REVIVED, +import getLogger from '../../test/fixtures/no-logger'; +import TagTypeService from './tag-type-service'; +import { ADDON_CONFIG_CREATED, - ADDON_CONFIG_UPDATED, ADDON_CONFIG_DELETED, -} = require('../types/events'); + ADDON_CONFIG_UPDATED, + FEATURE_CREATED, +} from '../types/events'; +import createStores from '../../test/fixtures/store'; + +import AddonService from './addon-service'; +import { IAddonDto } from '../types/stores/addon-store'; +import SimpleAddon from './addon-service-test-simple-addon'; const MASKED_VALUE = '*****'; -const definition = { - name: 'simple', - displayName: 'Simple ADdon', - description: 'Some description', - parameters: [ - { - name: 'url', - displayName: 'Some URL', - type: 'url', - required: true, - }, - { - name: 'var', - displayName: 'Some var', - description: 'Some variable to inject', - type: 'text', - required: false, - }, - { - name: 'sensitiveParam', - displayName: 'Some sensitive param', - description: 'Some variable to inject', - type: 'text', - required: false, - sensitive: true, - }, - ], - events: [ - FEATURE_CREATED, - FEATURE_UPDATED, - FEATURE_ARCHIVED, - FEATURE_REVIVED, - ], - tagTypes: [ - { - name: 'me', - description: 'Some tag', - icon: 'm', - }, - ], -}; - -class SimpleAddon extends Addon { - constructor() { - super(definition, { getLogger }); - this.events = []; - } - - getEvents() { - return this.events; - } - - async handleEvent(event, parameters) { - this.events.push({ - event, - parameters, - }); - } -} - -jest.mock('../addons', () => new Array(SimpleAddon)); - -const AddonService = require('./addon-service'); +const addonProvider = { simple: new SimpleAddon() }; function getSetup() { - const stores = store.createStores(); + const stores = createStores(); const tagTypeService = new TagTypeService(stores, { getLogger }); + return { addonService: new AddonService( stores, - { getLogger, server: { unleashUrl: 'http://test' } }, + { + getLogger, + // @ts-ignore + server: { unleashUrl: 'http://test' }, + }, tagTypeService, + addonProvider, ), stores, tagTypeService, @@ -107,9 +49,9 @@ test('should load addon configurations', async () => { test('should load provider definitions', async () => { const { addonService } = getSetup(); - const providerDefinitions = await addonService.getProviderDefinition(); + const providerDefinitions = await addonService.getProviderDefinitions(); - const simple = providerDefinitions.find(p => p.name === 'simple'); + const simple = providerDefinitions.find((p) => p.name === 'simple'); expect(providerDefinitions.length).toBe(1); expect(simple.name).toBe('simple'); @@ -119,7 +61,16 @@ test('should not allow addon-config for unknown provider', async () => { const { addonService } = getSetup(); await expect(async () => { - await addonService.createAddon({ provider: 'unknown' }); + await addonService.createAddon( + { + provider: 'unknown', + enabled: true, + parameters: {}, + events: [], + description: '', + }, + 'test', + ); }).rejects.toThrow(new TypeError('Unknown addon provider unknown')); }); @@ -134,6 +85,7 @@ test('should trigger simple-addon eventHandler', async () => { var: 'some-value', }, events: [FEATURE_CREATED], + description: '', }; await addonService.createAddon(config, 'me@mail.com'); @@ -150,6 +102,7 @@ test('should trigger simple-addon eventHandler', async () => { }); const simpleProvider = addonService.addonProviders.simple; + // @ts-ignore const events = simpleProvider.getEvents(); expect(events.length).toBe(1); @@ -160,7 +113,7 @@ test('should trigger simple-addon eventHandler', async () => { test('should create simple-addon config', async () => { const { addonService } = getSetup(); - const config = { + const config: IAddonDto = { provider: 'simple', enabled: true, parameters: { @@ -168,6 +121,7 @@ test('should create simple-addon config', async () => { var: 'some-value', }, events: [FEATURE_CREATED], + description: '', }; await addonService.createAddon(config, 'me@mail.com'); @@ -180,7 +134,7 @@ test('should create simple-addon config', async () => { test('should create tag type for simple-addon', async () => { const { addonService, tagTypeService } = getSetup(); - const config = { + const config: IAddonDto = { provider: 'simple', enabled: true, parameters: { @@ -188,6 +142,7 @@ test('should create tag type for simple-addon', async () => { var: 'some-value', }, events: [FEATURE_CREATED], + description: '', }; await addonService.createAddon(config, 'me@mail.com'); @@ -207,6 +162,7 @@ test('should store ADDON_CONFIG_CREATE event', async () => { var: 'some-value', }, events: [FEATURE_CREATED], + description: '', }; await addonService.createAddon(config, 'me@mail.com'); @@ -221,7 +177,8 @@ test('should store ADDON_CONFIG_CREATE event', async () => { test('should store ADDON_CONFIG_UPDATE event', async () => { const { addonService, stores } = getSetup(); - const config = { + const config: IAddonDto = { + description: '', provider: 'simple', enabled: true, parameters: { @@ -246,8 +203,9 @@ test('should store ADDON_CONFIG_UPDATE event', async () => { test('should store ADDON_CONFIG_REMOVE event', async () => { const { addonService, stores } = getSetup(); - const config = { + const config: IAddonDto = { provider: 'simple', + description: '', enabled: true, parameters: { url: 'http://localhost/wh', @@ -270,9 +228,10 @@ test('should store ADDON_CONFIG_REMOVE event', async () => { test('should hide sensitive fields when fetching', async () => { const { addonService } = getSetup(); - const config = { + const config: IAddonDto = { provider: 'simple', enabled: true, + description: '', parameters: { url: 'http://localhost/wh', var: 'some-value', @@ -286,14 +245,16 @@ test('should hide sensitive fields when fetching', async () => { const addonRetrieved = await addonService.getAddon(createdConfig.id); expect(addons.length).toBe(1); + // @ts-ignore expect(addons[0].parameters.sensitiveParam).toBe(MASKED_VALUE); + // @ts-ignore expect(addonRetrieved.parameters.sensitiveParam).toBe(MASKED_VALUE); }); test('should not overwrite masked values when updating', async () => { const { addonService, stores } = getSetup(); - const config = { + const config: IAddonDto = { provider: 'simple', enabled: true, parameters: { @@ -301,6 +262,7 @@ test('should not overwrite masked values when updating', async () => { var: 'some-value', }, events: [FEATURE_CREATED], + description: '', }; const addonConfig = await addonService.createAddon(config, 'me@mail.com'); @@ -313,7 +275,9 @@ test('should not overwrite masked values when updating', async () => { await addonService.updateAddon(addonConfig.id, updated, 'me@mail.com'); const updatedConfig = await stores.addonStore.get(addonConfig.id); + // @ts-ignore expect(updatedConfig.parameters.url).toBe('http://localhost/wh'); + // @ts-ignore expect(updatedConfig.parameters.var).toBe('some-new-value'); }); @@ -327,6 +291,7 @@ test('should reject addon config with missing required parameter when creating', var: 'some-value', }, events: [FEATURE_CREATED], + description: '', }; await expect(async () => @@ -345,6 +310,7 @@ test('should reject updating addon config with missing required parameter', asyn var: 'some-value', }, events: [FEATURE_CREATED], + description: '', }; const config = await addonService.createAddon(addonConfig, 'me@mail.com'); @@ -369,6 +335,7 @@ test('Should reject addon config if a required parameter is just the empty strin var: 'some-value', }, events: [FEATURE_CREATED], + description: '', }; await expect(async () => diff --git a/src/lib/services/addon-service.js b/src/lib/services/addon-service.ts similarity index 57% rename from src/lib/services/addon-service.js rename to src/lib/services/addon-service.ts index f407d9799a..e6fc6f23fb 100644 --- a/src/lib/services/addon-service.js +++ b/src/lib/services/addon-service.ts @@ -1,68 +1,90 @@ -'use strict'; +import memoizee from 'memoizee'; +import { ValidationError } from 'joi'; +import { getAddons, IAddonProviders } from '../addons'; +import * as events from '../types/events'; +import { addonSchema } from './addon-schema'; +import NameExistsError from '../error/name-exists-error'; +import { IEventStore } from '../types/stores/event-store'; +import { IFeatureToggleStore } from '../types/stores/feature-toggle-store'; +import { Logger } from '../logger'; +import TagTypeService from './tag-type-service'; +import { IAddon, IAddonDto, IAddonStore } from '../types/stores/addon-store'; +import { IUnleashStores } from '../types/stores'; +import { IUnleashConfig } from '../types/option'; +import { IAddonDefinition } from '../types/model'; -const memoize = require('memoizee'); -const { ValidationError } = require('joi'); -const addonProvidersClasses = require('../addons'); -const events = require('../types/events'); -const { addonSchema } = require('./addon-schema'); -const NameExistsError = require('../error/name-exists-error'); - -const SUPPORTED_EVENTS = Object.keys(events).map(k => events[k]); +const SUPPORTED_EVENTS = Object.keys(events).map((k) => events[k]); const ADDONS_CACHE_TIME = 60 * 1000; // 60s const MASKED_VALUE = '*****'; -class AddonService { +interface ISensitiveParams { + [key: string]: string[]; +} +export default class AddonService { + eventStore: IEventStore; + + addonStore: IAddonStore; + + featureToggleStore: IFeatureToggleStore; + + logger: Logger; + + tagTypeService: TagTypeService; + + addonProviders: IAddonProviders; + + sensitiveParams: ISensitiveParams; + + fetchAddonConfigs: (() => Promise) & + memoizee.Memoized<() => Promise>; + constructor( - { addonStore, eventStore, featureToggleStore }, - config, - tagTypeService, + { + addonStore, + eventStore, + featureToggleStore, + }: Pick< + IUnleashStores, + 'addonStore' | 'eventStore' | 'featureToggleStore' + >, + { getLogger, server }: Pick, + tagTypeService: TagTypeService, + addons?: IAddonProviders, ) { this.eventStore = eventStore; this.addonStore = addonStore; this.featureToggleStore = featureToggleStore; - this.getLogger = config.getLogger; - this.logger = config.getLogger('services/addon-service.js'); + this.logger = getLogger('services/addon-service.js'); this.tagTypeService = tagTypeService; - this.addonProviders = this.loadProviders({ - getLogger: config.getLogger, - unleashUrl: config.server.unleashUrl, - }); + this.addonProviders = + addons || + getAddons({ + getLogger, + unleashUrl: server.unleashUrl, + }); this.sensitiveParams = this.loadSensitiveParams(this.addonProviders); if (addonStore) { this.registerEventHandler(); } // Memoized private function - this.fetchAddonConfigs = memoize( - () => addonStore.getAll({ enabled: true }), - { promise: true, maxAge: ADDONS_CACHE_TIME }, - ); + this.fetchAddonConfigs = memoizee(async () => addonStore.getAll(), { + promise: true, + maxAge: ADDONS_CACHE_TIME, + }); } - loadProviders(config) { - return addonProvidersClasses.reduce((map, Provider) => { - try { - const provider = new Provider(config); - // eslint-disable-next-line no-param-reassign - map[provider.name] = provider; - } finally { - // Do nothing - } - return map; - }, {}); - } - - loadSensitiveParams(addonProviders) { + loadSensitiveParams(addonProviders: IAddonProviders): ISensitiveParams { const providerDefinitions = Object.values(addonProviders).map( - p => p.definition, + (p) => p.definition, ); return providerDefinitions.reduce((obj, definition) => { const sensitiveParams = definition.parameters - .filter(p => p.sensitive) - .map(p => p.name); + .filter((p) => p.sensitive) + .map((p) => p.name); const o = { ...obj }; o[definition.name] = sensitiveParams; @@ -70,20 +92,20 @@ class AddonService { }, {}); } - registerEventHandler() { - SUPPORTED_EVENTS.forEach(eventName => + registerEventHandler(): void { + SUPPORTED_EVENTS.forEach((eventName) => this.eventStore.on(eventName, this.handleEvent(eventName)), ); } - handleEvent(eventName) { + handleEvent(eventName: string): (IEvent) => void { const { addonProviders } = this; - return event => { - this.fetchAddonConfigs().then(addonInstances => { + return (event) => { + this.fetchAddonConfigs().then((addonInstances) => { addonInstances - .filter(addon => addon.events.includes(eventName)) - .filter(addon => addonProviders[addon.provider]) - .forEach(addon => + .filter((addon) => addon.events.includes(eventName)) + .filter((addon) => addonProviders[addon.provider]) + .forEach((addon) => addonProviders[addon.provider].handleEvent( event, addon.parameters, @@ -94,12 +116,12 @@ class AddonService { } // Should be used by the controller. - async getAddons() { + async getAddons(): Promise { const addonConfigs = await this.addonStore.getAll(); - return addonConfigs.map(a => this.filterSensitiveFields(a)); + return addonConfigs.map((a) => this.filterSensitiveFields(a)); } - filterSensitiveFields(addonConfig) { + filterSensitiveFields(addonConfig: IAddon): IAddon { const { sensitiveParams } = this; const a = { ...addonConfig }; a.parameters = Object.keys(a.parameters).reduce((obj, paramKey) => { @@ -115,24 +137,27 @@ class AddonService { return a; } - async getAddon(id) { + async getAddon(id: number): Promise { const addonConfig = await this.addonStore.get(id); return this.filterSensitiveFields(addonConfig); } - getProviderDefinition() { + getProviderDefinitions(): IAddonDefinition[] { const { addonProviders } = this; - return Object.values(addonProviders).map(p => p.definition); + return Object.values(addonProviders).map((p) => p.definition); } - async addTagTypes(providerName) { + async addTagTypes(providerName: string): Promise { const provider = this.addonProviders[providerName]; if (provider) { const tagTypes = provider.definition.tagTypes || []; - const createTags = tagTypes.map(async tagType => { + const createTags = tagTypes.map(async (tagType) => { try { await this.tagTypeService.validateUnique(tagType); - await this.tagTypeService.createTagType(tagType); + await this.tagTypeService.createTagType( + tagType, + providerName, + ); } catch (err) { this.logger.error(err); if (!(err instanceof NameExistsError)) { @@ -140,12 +165,12 @@ class AddonService { } } }); - return Promise.all(createTags); + await Promise.all(createTags); } return Promise.resolve(); } - async createAddon(data, userName) { + async createAddon(data: IAddonDto, userName: string): Promise { const addonConfig = await addonSchema.validateAsync(data); await this.validateKnownProvider(addonConfig); await this.validateRequiredParameters(addonConfig); @@ -166,7 +191,11 @@ class AddonService { return createdAddon; } - async updateAddon(id, data, userName) { + async updateAddon( + id: number, + data: IAddonDto, + userName: string, + ): Promise { const addonConfig = await addonSchema.validateAsync(data); await this.validateRequiredParameters(addonConfig); if (this.sensitiveParams[addonConfig.provider].length > 0) { @@ -193,7 +222,7 @@ class AddonService { this.logger.info(`User ${userName} updated addon ${id}`); } - async removeAddon(id, userName) { + async removeAddon(id: number, userName: string): Promise { await this.addonStore.delete(id); await this.eventStore.store({ type: events.ADDON_CONFIG_DELETED, @@ -203,7 +232,7 @@ class AddonService { this.logger.info(`User ${userName} removed addon ${id}`); } - async validateKnownProvider(config) { + async validateKnownProvider(config: Partial): Promise { const p = this.addonProviders[config.provider]; if (!p) { throw new TypeError(`Unknown addon provider ${config.provider}`); @@ -212,14 +241,18 @@ class AddonService { } } - async validateRequiredParameters({ provider, parameters }) { + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + async validateRequiredParameters({ + provider, + parameters, + }): Promise { const providerDefinition = this.addonProviders[provider].definition; const requiredParamsMissing = providerDefinition.parameters - .filter(p => p.required) - .map(p => p.name) + .filter((p) => p.required) + .map((p) => p.name) .filter( - requiredParam => + (requiredParam) => !Object.keys(parameters).includes(requiredParam), ); if (requiredParamsMissing.length > 0) { @@ -228,10 +261,9 @@ class AddonService { ',', )} `, '', + undefined, ); } return true; } } - -module.exports = AddonService; diff --git a/src/lib/services/api-token-service.ts b/src/lib/services/api-token-service.ts index 33e08290fa..5ae12ee7d3 100644 --- a/src/lib/services/api-token-service.ts +++ b/src/lib/services/api-token-service.ts @@ -1,10 +1,14 @@ import crypto from 'crypto'; -import { ApiTokenStore, IApiToken, ApiTokenType } from '../db/api-token-store'; import { Logger } from '../logger'; import { ADMIN, CLIENT } from '../types/permissions'; import { IUnleashStores } from '../types/stores'; import { IUnleashConfig } from '../types/option'; import ApiUser from '../types/api-user'; +import { + ApiTokenType, + IApiToken, + IApiTokenStore, +} from '../types/stores/api-token-store'; const ONE_MINUTE = 60_000; @@ -15,7 +19,7 @@ interface CreateTokenRequest { } export class ApiTokenService { - private store: ApiTokenStore; + private store: IApiTokenStore; private logger: Logger; @@ -24,10 +28,10 @@ export class ApiTokenService { private activeTokens: IApiToken[] = []; constructor( - stores: Pick, + { apiTokenStore }: Pick, config: Pick, ) { - this.store = stores.apiTokenStore; + this.store = apiTokenStore; this.logger = config.getLogger('/services/api-token-service.ts'); this.fetchActiveTokens(); this.timer = setInterval( @@ -54,7 +58,7 @@ export class ApiTokenService { } public getUserForToken(secret: string): ApiUser | undefined { - const token = this.activeTokens.find(t => t.secret === secret); + const token = this.activeTokens.find((t) => t.secret === secret); if (token) { const permissions = token.type === ApiTokenType.ADMIN ? [ADMIN] : [CLIENT]; diff --git a/src/lib/services/client-metrics/client-metrics-schema.ts b/src/lib/services/client-metrics/client-metrics-schema.ts index c66a7ebb52..ae10825bca 100644 --- a/src/lib/services/client-metrics/client-metrics-schema.ts +++ b/src/lib/services/client-metrics/client-metrics-schema.ts @@ -4,16 +4,8 @@ const countSchema = joi .object() .options({ stripUnknown: true }) .keys({ - yes: joi - .number() - .min(0) - .empty('') - .default(0), - no: joi - .number() - .min(0) - .empty('') - .default(0), + yes: joi.number().min(0).empty('').default(0), + no: joi.number().min(0).empty('').default(0), variants: joi.object().pattern(joi.string(), joi.number().min(0)), }); diff --git a/src/lib/services/client-metrics/client-metrics.test.ts b/src/lib/services/client-metrics/client-metrics.test.ts index 3be7bac14a..e0c711f8cb 100644 --- a/src/lib/services/client-metrics/client-metrics.test.ts +++ b/src/lib/services/client-metrics/client-metrics.test.ts @@ -1,12 +1,13 @@ import EventEmitter from 'events'; import moment from 'moment'; -import ClientMetricsService, { IClientApp } from './index'; +import ClientMetricsService from './index'; import getLogger from '../../../test/fixtures/no-logger'; +import { IClientApp } from '../../types/model'; const appName = 'appName'; const instanceId = 'instanceId'; -const createMetricsService = cms => +const createMetricsService = (cms) => new ClientMetricsService( { clientMetricsStore: cms, @@ -236,7 +237,7 @@ test('should have correct values for lastMinute', () => { }, ]; - input.forEach(bucket => { + input.forEach((bucket) => { clientMetricsStore.emit('metrics', { appName, instanceId, @@ -301,7 +302,7 @@ test('should have correct values for lastHour', () => { }, ]; - input.forEach(bucket => { + input.forEach((bucket) => { clientMetricsStore.emit('metrics', { appName, instanceId, diff --git a/src/lib/services/client-metrics/index.ts b/src/lib/services/client-metrics/index.ts index ec4d1c6fcc..6991307996 100644 --- a/src/lib/services/client-metrics/index.ts +++ b/src/lib/services/client-metrics/index.ts @@ -1,10 +1,3 @@ -/* eslint-disable no-param-reassign */ -import EventStore, { ICreateEvent } from '../../db/event-store'; -import StrategyStore from '../../db/strategy-store'; -import ClientApplicationsDb from '../../db/client-applications-store'; -import ClientInstanceStore from '../../db/client-instance-store'; -import { ClientMetricsStore } from '../../db/client-metrics-store'; -import FeatureToggleStore from '../../db/feature-toggle-store'; import { LogProvider } from '../../logger'; import { applicationSchema } from './metrics-schema'; import { Projection } from './projection'; @@ -13,62 +6,29 @@ import { APPLICATION_CREATED } from '../../types/events'; import { IApplication, IYesNoCount } from './models'; import { IUnleashStores } from '../../types/stores'; import { IUnleashConfig } from '../../types/option'; +import { IEventStore } from '../../types/stores/event-store'; +import { + IClientApplication, + IClientApplicationsStore, +} from '../../types/stores/client-applications-store'; +import { IFeatureToggleStore } from '../../types/stores/feature-toggle-store'; +import { IStrategyStore } from '../../types/stores/strategy-store'; +import { IClientMetricsStore } from '../../types/stores/client-metrics-store'; +import { IClientInstanceStore } from '../../types/stores/client-instance-store'; +import { IApplicationQuery } from '../../types/query'; +import { + IClientApp, + ICreateEvent, + IMetricCounts, + IMetricsBucket, +} from '../../types/model'; -const TTLList = require('./ttl-list'); -const { clientRegisterSchema } = require('./register-schema'); +import TTLList = require('./ttl-list'); +import { clientRegisterSchema } from './register-schema'; const FIVE_SECONDS = 5 * 1000; const FIVE_MINUTES = 5 * 60 * 1000; -export interface IClientApp { - appName: string; - instanceId: string; - clientIp?: string; - seenToggles?: string[]; - metricsCount?: number; - strategies?: string[] | Record[]; - bucket?: any; - count?: number; - started?: number | Date; - interval?: number; - icon?: string; - description?: string; - color?: string; -} - -export interface IAppFeature { - name: string; - description: string; - type: string; - project: string; - enabled: boolean; - stale: boolean; - strategies: any; - variants: any[]; - createdAt: Date; - lastSeenAt: Date; -} - -export interface IApplicationQuery { - strategyName?: string; -} - -export interface IAppName { - appName: string; -} - -export interface IMetricCounts { - yes?: number; - no?: number; - variants?: Record; -} - -export interface IMetricsBucket { - start: Date; - stop: Date; - toggles: IMetricCounts; -} - export default class ClientMetricsService { globalCount = 0; @@ -94,17 +54,17 @@ export default class ClientMetricsService { private timers: NodeJS.Timeout[] = []; - private clientMetricsStore: ClientMetricsStore; + private clientMetricsStore: IClientMetricsStore; - private strategyStore: StrategyStore; + private strategyStore: IStrategyStore; - private featureToggleStore: FeatureToggleStore; + private featureToggleStore: IFeatureToggleStore; - private clientApplicationsStore: ClientApplicationsDb; + private clientApplicationsStore: IClientApplicationsStore; - private clientInstanceStore: ClientInstanceStore; + private clientInstanceStore: IClientInstanceStore; - private eventStore: EventStore; + private eventStore: IEventStore; private getLogger: LogProvider; @@ -120,13 +80,14 @@ export default class ClientMetricsService { clientInstanceStore, clientApplicationsStore, eventStore, - }: Pick, { getLogger }: Pick, bulkInterval = FIVE_SECONDS, @@ -144,16 +105,16 @@ export default class ClientMetricsService { this.bulkInterval = bulkInterval; this.announcementInterval = announcementInterval; - this.lastHourList.on('expire', toggles => { - Object.keys(toggles).forEach(toggleName => { + this.lastHourList.on('expire', (toggles) => { + Object.keys(toggles).forEach((toggleName) => { this.lastHourProjection.substract( toggleName, this.createCountObject(toggles[toggleName]), ); }); }); - this.lastMinuteList.on('expire', toggles => { - Object.keys(toggles).forEach(toggleName => { + this.lastMinuteList.on('expire', (toggles) => { + Object.keys(toggles).forEach((toggleName) => { this.lastMinuteProjection.substract( toggleName, this.createCountObject(toggles[toggleName]), @@ -170,7 +131,7 @@ export default class ClientMetricsService { this.announcementInterval, ).unref(), ); - clientMetricsStore.on('metrics', m => this.addPayload(m)); + clientMetricsStore.on('metrics', (m) => this.addPayload(m)); } async registerClientMetrics( @@ -179,7 +140,7 @@ export default class ClientMetricsService { ): Promise { const value = await clientMetricsSchema.validateAsync(data); const toggleNames = Object.keys(value.bucket.toggles); - await this.featureToggleStore.lastSeenToggles(toggleNames); + await this.featureToggleStore.updateLastSeenForToggles(toggleNames); await this.clientMetricsStore.insert(value); await this.clientInstanceStore.insert({ appName: value.appName, @@ -190,9 +151,10 @@ export default class ClientMetricsService { async announceUnannounced(): Promise { if (this.clientApplicationsStore) { - const appsToAnnounce = await this.clientApplicationsStore.setUnannouncedToAnnounced(); + const appsToAnnounce = + await this.clientApplicationsStore.setUnannouncedToAnnounced(); if (appsToAnnounce.length > 0) { - const events = appsToAnnounce.map(app => ({ + const events = appsToAnnounce.map((app) => ({ type: APPLICATION_CREATED, createdBy: app.createdBy || 'unknown', data: app, @@ -223,6 +185,7 @@ export default class ClientMetricsService { const uniqueRegistrations = Object.values(this.seenClients); const uniqueApps = Object.values( uniqueRegistrations.reduce((soFar, reg) => { + // eslint-disable-next-line no-param-reassign soFar[reg.appName] = reg; return soFar; }, {}), @@ -251,7 +214,7 @@ export default class ClientMetricsService { getAppsWithToggles(): IClientApp[] { const apps = []; - Object.keys(this.apps).forEach(appName => { + Object.keys(this.apps).forEach((appName) => { const seenToggles = Object.keys(this.apps[appName].seenToggles); const metricsCount = this.apps[appName].count; apps.push({ appName, seenToggles, metricsCount }); @@ -267,15 +230,16 @@ export default class ClientMetricsService { async getSeenApps(): Promise> { const seenApps = this.getSeenAppsPerToggle(); - const applications: IApplication[] = await this.clientApplicationsStore.getApplications(); + const applications: IClientApplication[] = + await this.clientApplicationsStore.getAll(); const metaData = applications.reduce((result, entry) => { // eslint-disable-next-line no-param-reassign result[entry.appName] = entry; return result; }, {}); - Object.keys(seenApps).forEach(key => { - seenApps[key] = seenApps[key].map(entry => { + Object.keys(seenApps).forEach((key) => { + seenApps[key] = seenApps[key].map((entry) => { if (metaData[entry.appName]) { return { ...entry, ...metaData[entry.appName] }; } @@ -287,23 +251,19 @@ export default class ClientMetricsService { async getApplications( query: IApplicationQuery, - ): Promise> { - return this.clientApplicationsStore.getApplications(query); + ): Promise { + return this.clientApplicationsStore.getAppsForStrategy(query); } async getApplication(appName: string): Promise { const seenToggles = this.getSeenTogglesByAppName(appName); - const [ - application, - instances, - strategies, - features, - ] = await Promise.all([ - this.clientApplicationsStore.getApplication(appName), - this.clientInstanceStore.getByAppName(appName), - this.strategyStore.getStrategies(), - this.featureToggleStore.getFeatures(), - ]); + const [application, instances, strategies, features] = + await Promise.all([ + this.clientApplicationsStore.get(appName), + this.clientInstanceStore.getByAppName(appName), + this.strategyStore.getAll(), + this.featureToggleStore.getFeatures(false), + ]); return { appName: application.appName, @@ -312,13 +272,13 @@ export default class ClientMetricsService { url: application.url, color: application.color, icon: application.icon, - strategies: application.strategies.map(name => { - const found = strategies.find(f => f.name === name); + strategies: application.strategies.map((name) => { + const found = strategies.find((f) => f.name === name); return found || { name, notFound: true }; }), instances, - seenToggles: seenToggles.map(name => { - const found = features.find(f => f.name === name); + seenToggles: seenToggles.map((name) => { + const found = features.find((f) => f.name === name); return found || { name, notFound: true }; }), links: { @@ -329,9 +289,9 @@ export default class ClientMetricsService { getSeenAppsPerToggle(): Record { const toggles = {}; - Object.keys(this.apps).forEach(appName => { + Object.keys(this.apps).forEach((appName) => { Object.keys(this.apps[appName].seenToggles).forEach( - seenToggleName => { + (seenToggleName) => { if (!toggles[seenToggleName]) { toggles[seenToggleName] = []; } @@ -387,7 +347,7 @@ export default class ClientMetricsService { const toggleNames = Object.keys(toggles); - toggleNames.forEach(n => { + toggleNames.forEach((n) => { const countObj = this.createCountObject(toggles[n]); this.lastHourProjection.add(n, countObj); this.lastMinuteProjection.add(n, countObj); @@ -398,19 +358,21 @@ export default class ClientMetricsService { this.lastMinuteList.add(toggles, stop); this.globalCount += count; + // eslint-disable-next-line no-param-reassign app.count += count; this.addSeenToggles(app, toggleNames); } addSeenToggles(app: IClientApp, toggleNames: string[]): void { - toggleNames.forEach(t => { + toggleNames.forEach((t) => { + // eslint-disable-next-line no-param-reassign app.seenToggles[t] = true; }); } async deleteApplication(appName: string): Promise { await this.clientInstanceStore.deleteForApplication(appName); - await this.clientApplicationsStore.deleteApplication(appName); + await this.clientApplicationsStore.delete(appName); } async createApplication(input: IApplication): Promise { diff --git a/src/lib/services/client-metrics/list.test.js b/src/lib/services/client-metrics/list.test.js index 1f484c1cf4..73a30e45fa 100644 --- a/src/lib/services/client-metrics/list.test.js +++ b/src/lib/services/client-metrics/list.test.js @@ -17,7 +17,7 @@ function getList() { test('should emit "evicted" events for objects leaving list', () => { const list = getList(); const evictedList = []; - list.on('evicted', value => { + list.on('evicted', (value) => { evictedList.push(value); }); diff --git a/src/lib/services/client-metrics/metrics-schema.ts b/src/lib/services/client-metrics/metrics-schema.ts index fafa31d84e..56b2479dd9 100644 --- a/src/lib/services/client-metrics/metrics-schema.ts +++ b/src/lib/services/client-metrics/metrics-schema.ts @@ -10,24 +10,9 @@ export const applicationSchema = joi .array() .optional() .items(joi.string(), joi.any().strip()), - description: joi - .string() - .allow('') - .optional(), - url: joi - .string() - .allow('') - .optional(), - color: joi - .string() - .allow('') - .optional(), - icon: joi - .string() - .allow('') - .optional(), - announced: joi - .boolean() - .optional() - .default(false), + description: joi.string().allow('').optional(), + url: joi.string().allow('').optional(), + color: joi.string().allow('').optional(), + icon: joi.string().allow('').optional(), + announced: joi.boolean().optional().default(false), }); diff --git a/src/lib/services/client-metrics/models.ts b/src/lib/services/client-metrics/models.ts index e745e9ff93..bff026260c 100644 --- a/src/lib/services/client-metrics/models.ts +++ b/src/lib/services/client-metrics/models.ts @@ -1,3 +1,5 @@ +import { IClientInstance } from '../../types/stores/client-instance-store'; + export interface IYesNoCount { yes: number; no: number; @@ -21,7 +23,7 @@ export interface IApplication { color?: string; icon?: string; createdAt: Date; - instances?: IAppInstance; + instances?: IClientInstance[]; seenToggles: Record; links: Record; } diff --git a/src/lib/services/client-metrics/projection.test.ts b/src/lib/services/client-metrics/projection.test.ts index 3eacb0ae38..62f7d504fd 100644 --- a/src/lib/services/client-metrics/projection.test.ts +++ b/src/lib/services/client-metrics/projection.test.ts @@ -1,6 +1,5 @@ import { Projection } from './projection'; - test('should return set empty if missing', () => { const projection = new Projection(); diff --git a/src/lib/services/client-metrics/register-schema.js b/src/lib/services/client-metrics/register-schema.ts similarity index 77% rename from src/lib/services/client-metrics/register-schema.js rename to src/lib/services/client-metrics/register-schema.ts index 9d0997cd11..6e41b9c1f1 100644 --- a/src/lib/services/client-metrics/register-schema.js +++ b/src/lib/services/client-metrics/register-schema.ts @@ -1,8 +1,6 @@ -'use strict'; +import joi from 'joi'; -const joi = require('joi'); - -const clientRegisterSchema = joi +export const clientRegisterSchema = joi .object() .options({ stripUnknown: true }) .keys({ @@ -16,5 +14,3 @@ const clientRegisterSchema = joi started: joi.date().required(), interval: joi.number().required(), }); - -module.exports = { clientRegisterSchema }; diff --git a/src/lib/services/client-metrics/ttl-list.test.js b/src/lib/services/client-metrics/ttl-list.test.js index 1eb44d25ad..c45bce20e3 100644 --- a/src/lib/services/client-metrics/ttl-list.test.js +++ b/src/lib/services/client-metrics/ttl-list.test.js @@ -3,7 +3,7 @@ const moment = require('moment'); const TTLList = require('./ttl-list'); -test('should emit expire', done => { +test('should emit expire', (done) => { jest.useFakeTimers('modern'); const list = new TTLList({ interval: 20, @@ -11,7 +11,7 @@ test('should emit expire', done => { expireType: 'milliseconds', }); - list.on('expire', entry => { + list.on('expire', (entry) => { list.destroy(); expect(entry.n).toBe(1); done(); @@ -38,7 +38,7 @@ test('should slice off list', () => { const expired = []; - list.on('expire', entry => { + list.on('expire', (entry) => { // console.timeEnd(entry.n); expired.push(entry); }); diff --git a/src/lib/services/context-schema.js b/src/lib/services/context-schema.js index 89e5205c2d..161ee36ccf 100644 --- a/src/lib/services/context-schema.js +++ b/src/lib/services/context-schema.js @@ -9,22 +9,14 @@ const contextSchema = joi .object() .keys({ name: nameType, - description: joi - .string() - .max(250) - .allow('') - .allow(null) - .optional(), + description: joi.string().max(250).allow('').allow(null).optional(), legalValues: joi .array() .allow(null) .unique() .optional() .items(joi.string().max(100)), - stickiness: joi - .boolean() - .optional() - .default(false), + stickiness: joi.boolean().optional().default(false), }) .options({ allowUnknown: false, stripUnknown: true }); diff --git a/src/lib/services/context-service.ts b/src/lib/services/context-service.ts index 8c5c39385e..305310453e 100644 --- a/src/lib/services/context-service.ts +++ b/src/lib/services/context-service.ts @@ -1,9 +1,13 @@ -'use strict'; - -import ContextFieldStore from '../db/context-field-store'; -import EventStore from '../db/event-store'; -import ProjectStore from '../db/project-store'; import { Logger } from '../logger'; +import { + IContextField, + IContextFieldDto, + IContextFieldStore, +} from '../types/stores/context-field-store'; +import { IEventStore } from '../types/stores/event-store'; +import { IProjectStore } from '../types/stores/project-store'; +import { IUnleashStores } from '../types/stores'; +import { IUnleashConfig } from '../types/option'; const { contextSchema, nameSchema } = require('./context-schema'); const NameExistsError = require('../error/name-exists-error'); @@ -15,17 +19,24 @@ const { } = require('../types/events'); class ContextService { - private projectStore: ProjectStore; + private projectStore: IProjectStore; - private eventStore: EventStore; + private eventStore: IEventStore; - private contextFieldStore: ContextFieldStore; + private contextFieldStore: IContextFieldStore; private logger: Logger; constructor( - { projectStore, eventStore, contextFieldStore }, - { getLogger }, + { + projectStore, + eventStore, + contextFieldStore, + }: Pick< + IUnleashStores, + 'projectStore' | 'eventStore' | 'contextFieldStore' + >, + { getLogger }: Pick, ) { this.projectStore = projectStore; this.eventStore = eventStore; @@ -33,15 +44,18 @@ class ContextService { this.logger = getLogger('services/context-service.js'); } - async getAll() { + async getAll(): Promise { return this.contextFieldStore.getAll(); } - async getContextField(name) { + async getContextField(name: string): Promise { return this.contextFieldStore.get(name); } - async createContextField(value, userName) { + async createContextField( + value: IContextFieldDto, + userName: string, + ): Promise { // validations await this.validateUniqueName(value); const contextField = await contextSchema.validateAsync(value); @@ -55,7 +69,10 @@ class ContextService { }); } - async updateContextField(updatedContextField, userName) { + async updateContextField( + updatedContextField: IContextFieldDto, + userName: string, + ): Promise { // validations await this.contextFieldStore.get(updatedContextField.name); const value = await contextSchema.validateAsync(updatedContextField); @@ -69,7 +86,7 @@ class ContextService { }); } - async deleteContextField(name, userName) { + async deleteContextField(name: string, userName: string): Promise { // validate existence await this.contextFieldStore.get(name); @@ -82,7 +99,9 @@ class ContextService { }); } - async validateUniqueName({ name }) { + async validateUniqueName({ + name, + }: Pick): Promise { let msg; try { await this.contextFieldStore.get(name); @@ -96,7 +115,7 @@ class ContextService { throw new NameExistsError(msg); } - async validateName(name) { + async validateName(name: string): Promise { await nameSchema.validateAsync({ name }); await this.validateUniqueName({ name }); } diff --git a/src/lib/services/email-service.test.ts b/src/lib/services/email-service.test.ts index 84e2ff1b2d..a72b56399a 100644 --- a/src/lib/services/email-service.test.ts +++ b/src/lib/services/email-service.test.ts @@ -1,4 +1,4 @@ -import { EmailService, TransporterType } from './email-service'; +import { EmailService } from './email-service'; import noLoggerProvider from '../../test/fixtures/no-logger'; test('Can send reset email', async () => { diff --git a/src/lib/services/email-service.ts b/src/lib/services/email-service.ts index a18cb4f155..30d1a0a4ca 100644 --- a/src/lib/services/email-service.ts +++ b/src/lib/services/email-service.ts @@ -109,7 +109,7 @@ export class EmailService { this.logger.info( 'Successfully sent reset-password email', ), - e => + (e) => this.logger.warn( 'Failed to send reset-password email', e, @@ -118,7 +118,7 @@ export class EmailService { }); return Promise.resolve(email); } - return new Promise(res => { + return new Promise((res) => { this.logger.warn( 'No mailer is configured. Please read the docs on how to configure an emailservice', ); @@ -163,7 +163,7 @@ export class EmailService { this.logger.info( 'Successfully sent getting started email', ), - e => + (e) => this.logger.warn( 'Failed to send getting started email', e, @@ -172,7 +172,7 @@ export class EmailService { }); return Promise.resolve(email); } - return new Promise(res => { + return new Promise((res) => { this.logger.warn( 'No mailer is configured. Please read the docs on how to configure an EmailService', ); diff --git a/src/lib/services/environment-service.ts b/src/lib/services/environment-service.ts index ba9be01507..549169e625 100644 --- a/src/lib/services/environment-service.ts +++ b/src/lib/services/environment-service.ts @@ -1,31 +1,41 @@ import { IUnleashStores } from '../types/stores'; import { IUnleashConfig } from '../types/option'; -import EnvironmentStore from '../db/environment-store'; import { Logger } from '../logger'; import { IEnvironment } from '../types/model'; -import FeatureStrategiesStore from '../db/feature-strategy-store'; import { UNIQUE_CONSTRAINT_VIOLATION } from '../error/db-error'; import NameExistsError from '../error/name-exists-error'; import { environmentSchema } from './state-schema'; import NotFoundError from '../error/notfound-error'; +import { IEnvironmentStore } from '../types/stores/environment-store'; +import { IFeatureStrategiesStore } from '../types/stores/feature-strategies-store'; +import { IFeatureEnvironmentStore } from '../types/stores/feature-environment-store'; export default class EnvironmentService { private logger: Logger; - private environmentStore: EnvironmentStore; + private environmentStore: IEnvironmentStore; - private featureStrategiesStore: FeatureStrategiesStore; + private featureStrategiesStore: IFeatureStrategiesStore; + + private featureEnvironmentStore: IFeatureEnvironmentStore; constructor( { environmentStore, featureStrategiesStore, - }: Pick, + featureEnvironmentStore, + }: Pick< + IUnleashStores, + | 'environmentStore' + | 'featureStrategiesStore' + | 'featureEnvironmentStore' + >, { getLogger }: Pick, ) { this.logger = getLogger('services/environment-service.ts'); this.environmentStore = environmentStore; this.featureStrategiesStore = featureStrategiesStore; + this.featureEnvironmentStore = featureEnvironmentStore; } async getAll(): Promise { @@ -33,7 +43,7 @@ export default class EnvironmentService { } async get(name: string): Promise { - return this.environmentStore.getByName(name); + return this.environmentStore.get(name); } async delete(name: string): Promise { @@ -77,7 +87,7 @@ export default class EnvironmentService { environment: string, projectId: string, ): Promise { - await this.featureStrategiesStore.disconnectEnvironmentFromProject( + await this.featureEnvironmentStore.disconnectEnvironmentFromProject( environment, projectId, ); diff --git a/src/lib/services/event-service.ts b/src/lib/services/event-service.ts index 78843c62b5..ebf50fe01a 100644 --- a/src/lib/services/event-service.ts +++ b/src/lib/services/event-service.ts @@ -1,12 +1,13 @@ import { IUnleashConfig } from '../types/option'; import { IUnleashStores } from '../types/stores'; import { Logger } from '../logger'; -import EventStore, { IEvent } from '../db/event-store'; +import { IEventStore } from '../types/stores/event-store'; +import { IEvent } from '../types/model'; export default class EventService { private logger: Logger; - private eventStore: EventStore; + private eventStore: IEventStore; constructor( { eventStore }: Pick, @@ -21,7 +22,7 @@ export default class EventService { } async getEventsForToggle(name: string): Promise { - return this.eventStore.getEventsFilterByName(name); + return this.eventStore.getEventsFilterByType(name); } } diff --git a/src/lib/services/feature-tag-service.ts b/src/lib/services/feature-tag-service.ts index 39a96b6c63..5d29c8c376 100644 --- a/src/lib/services/feature-tag-service.ts +++ b/src/lib/services/feature-tag-service.ts @@ -1,6 +1,3 @@ -import EventStore from '../db/event-store'; -import FeatureTagStore from '../db/feature-tag-store'; -import TagStore, { ITag } from '../db/tag-store'; import NotFoundError from '../error/notfound-error'; import { Logger } from '../logger'; import { nameSchema } from '../schema/feature-schema'; @@ -8,13 +5,17 @@ import { FEATURE_TAGGED, FEATURE_UNTAGGED, TAG_CREATED } from '../types/events'; import { IUnleashConfig } from '../types/option'; import { IUnleashStores } from '../types/stores'; import { tagSchema } from './tag-schema'; +import { IFeatureTagStore } from '../types/stores/feature-tag-store'; +import { IEventStore } from '../types/stores/event-store'; +import { ITagStore } from '../types/stores/tag-store'; +import { ITag } from '../types/model'; class FeatureTagService { - private tagStore: TagStore; + private tagStore: ITagStore; - private featureTagStore: FeatureTagStore; + private featureTagStore: IFeatureTagStore; - private eventStore: EventStore; + private eventStore: IEventStore; private logger: Logger; diff --git a/src/lib/services/feature-toggle-service-v2.ts b/src/lib/services/feature-toggle-service-v2.ts index d1f83e1d2a..59dbb4880b 100644 --- a/src/lib/services/feature-toggle-service-v2.ts +++ b/src/lib/services/feature-toggle-service-v2.ts @@ -2,56 +2,57 @@ import { IUnleashConfig } from '../types/option'; import { IUnleashStores } from '../types/stores'; import { Logger } from '../logger'; -import FeatureStrategiesStore, { - FeatureConfigurationClient, - IFeatureStrategy, -} from '../db/feature-strategy-store'; -import FeatureToggleStore from '../db/feature-toggle-store'; -import { - FeatureToggle, - FeatureToggleDTO, - FeatureToggleWithEnvironment, - IFeatureEnvironmentInfo, IFeatureOverview, - IFeatureToggleQuery, IProjectHealthReport, - IProjectOverview, - IStrategyConfig -} from '../types/model'; -import ProjectStore from '../db/project-store'; import BadDataError from '../error/bad-data-error'; import { FOREIGN_KEY_VIOLATION } from '../error/db-error'; import NameExistsError from '../error/name-exists-error'; import { featureMetadataSchema, nameSchema } from '../schema/feature-schema'; -import EventStore from '../db/event-store'; import { FEATURE_ARCHIVED, - FEATURE_CREATED, FEATURE_DELETED, FEATURE_REVIVED, + FEATURE_CREATED, + FEATURE_DELETED, + FEATURE_REVIVED, FEATURE_STALE_OFF, FEATURE_STALE_ON, - FEATURE_UPDATED + FEATURE_UPDATED, } from '../types/events'; -import FeatureTagStore from '../db/feature-tag-store'; -import EnvironmentStore from '../db/environment-store'; import { GLOBAL_ENV } from '../types/environment'; import NotFoundError from '../error/notfound-error'; -import FeatureTypeStore from '../db/feature-type-store'; -import { MILLISECONDS_IN_DAY } from '../util/constants'; +import { FeatureConfigurationClient, IFeatureStrategiesStore } from '../types/stores/feature-strategies-store'; +import { IFeatureTypeStore } from '../types/stores/feature-type-store'; +import { IEventStore } from '../types/stores/event-store'; +import { IEnvironmentStore } from '../types/stores/environment-store'; +import { IProjectStore } from '../types/stores/project-store'; +import { IFeatureTagStore } from '../types/stores/feature-tag-store'; +import { IFeatureToggleStore } from '../types/stores/feature-toggle-store'; +import { + FeatureToggle, + FeatureToggleDTO, + FeatureToggleWithEnvironment, + IFeatureEnvironmentInfo, + IFeatureStrategy, + IFeatureToggleQuery, + IStrategyConfig, +} from '../types/model'; +import { IFeatureEnvironmentStore } from '../types/stores/feature-environment-store'; class FeatureToggleServiceV2 { private logger: Logger; - private featureStrategiesStore: FeatureStrategiesStore; + private featureStrategiesStore: IFeatureStrategiesStore; - private featureToggleStore: FeatureToggleStore; + private featureToggleStore: IFeatureToggleStore; - private featureTagStore: FeatureTagStore; + private featureTagStore: IFeatureTagStore; - private projectStore: ProjectStore; + private featureEnvironmentStore: IFeatureEnvironmentStore; - private environmentStore: EnvironmentStore; + private projectStore: IProjectStore; - private eventStore: EventStore; + private environmentStore: IEnvironmentStore; - private featureTypeStore: FeatureTypeStore; + private eventStore: IEventStore; + + private featureTypeStore: IFeatureTypeStore; constructor( { @@ -62,6 +63,7 @@ class FeatureToggleServiceV2 { featureTagStore, environmentStore, featureTypeStore, + featureEnvironmentStore, }: Pick< IUnleashStores, | 'featureStrategiesStore' @@ -71,6 +73,7 @@ class FeatureToggleServiceV2 { | 'featureTagStore' | 'environmentStore' | 'featureTypeStore' + | 'featureEnvironmentStore' >, { getLogger }: Pick, ) { @@ -82,6 +85,7 @@ class FeatureToggleServiceV2 { this.eventStore = eventStore; this.environmentStore = environmentStore; this.featureTypeStore = featureTypeStore; + this.featureEnvironmentStore = featureEnvironmentStore; } async createStrategy( @@ -129,7 +133,7 @@ class FeatureToggleServiceV2 { id: string, updates: Partial, ): Promise { - const exists = await this.featureStrategiesStore.hasStrategy(id); + const exists = await this.featureStrategiesStore.exists(id); if (exists) { return this.featureStrategiesStore.updateStrategy(id, updates); } @@ -141,7 +145,7 @@ class FeatureToggleServiceV2 { featureName: string, environment: string = GLOBAL_ENV, ): Promise { - const hasEnv = await this.featureStrategiesStore.featureHasEnvironment( + const hasEnv = await this.featureEnvironmentStore.featureHasEnvironment( environment, featureName, ); @@ -151,7 +155,7 @@ class FeatureToggleServiceV2 { featureName, environment, ); - return featureStrategies.map(strat => ({ + return featureStrategies.map((strat) => ({ id: strat.id, name: strat.strategyName, constraints: strat.constraints, @@ -170,7 +174,7 @@ class FeatureToggleServiceV2 { */ async getFeature( featureName: string, - archived: boolean = false + archived: boolean = false, ): Promise { return this.featureStrategiesStore.getFeatureToggleAdmin(featureName, archived); } @@ -191,7 +195,7 @@ class FeatureToggleServiceV2 { */ async getFeatureToggles( query?: IFeatureToggleQuery, - archived: boolean = false + archived: boolean = false, ): Promise { return this.featureStrategiesStore.getFeatures(query, archived, true); } @@ -199,7 +203,7 @@ class FeatureToggleServiceV2 { async getFeatureToggle( featureName: string, ): Promise { - return this.featureStrategiesStore.getFeatureToggleAdmin(featureName); + return this.featureStrategiesStore.getFeatureToggleAdmin(featureName, false); } async createFeatureToggle( @@ -209,32 +213,26 @@ class FeatureToggleServiceV2 { ): Promise { this.logger.info(`${userName} creates feature toggle ${value.name}`); await this.validateName(value.name); - await this.projectStore.hasProject(projectId); - const featureData = await featureMetadataSchema.validateAsync(value); - const createdToggle = await this.featureToggleStore.createFeature( - projectId, - featureData, - ); - await this.environmentStore.connectFeatureToEnvironmentsForProject( - featureData.name, - projectId, - ); - await this.eventStore.store({ - type: FEATURE_CREATED, - createdBy: userName, - data: featureData, - }); + const exists = await this.projectStore.hasProject(projectId); + if (exists) { + const featureData = await featureMetadataSchema.validateAsync(value); + const createdToggle = await this.featureToggleStore.createFeature( + projectId, + featureData, + ); + await this.environmentStore.connectFeatureToEnvironmentsForProject( + featureData.name, + projectId, + ); + await this.eventStore.store({ + type: FEATURE_CREATED, + createdBy: userName, + data: featureData, + }); - return createdToggle; - } - - /** - * @deprecated - * @param featureName - * @returns - */ - async getProjectId(featureName: string): Promise { - return this.featureToggleStore.getProjectId(featureName); + return createdToggle; + } + throw new NotFoundError(`Project with id ${projectId} does not exist`); } async updateFeatureToggle( @@ -252,10 +250,9 @@ class FeatureToggleServiceV2 { projectId, updatedFeature, ); - const tags = - (await this.featureTagStore.getAllTagsForFeature( - updatedFeature.name, - )) || []; + const tags = (await this.featureTagStore.getAllTagsForFeature( + updatedFeature.name, + )) || []; await this.eventStore.store({ type: FEATURE_UPDATED, createdBy: userName, @@ -294,16 +291,12 @@ class FeatureToggleServiceV2 { }; } - - - - async getEnvironmentInfo( project: string, environment: string, featureName: string, ): Promise { - const envMetadata = await this.featureStrategiesStore.getEnvironmentMetaData( + const envMetadata = await this.featureEnvironmentStore.getEnvironmentMetaData( environment, featureName, ); @@ -328,7 +321,7 @@ class FeatureToggleServiceV2 { projectId, environment, ); - await this.projectStore.deleteEnvironment(projectId, environment); + await this.projectStore.deleteEnvironmentForProject(projectId, environment); } /** Validations */ @@ -365,9 +358,8 @@ class FeatureToggleServiceV2 { ); feature.stale = isStale; await this.featureToggleStore.updateFeature(feature.project, feature); - const tags = - (await this.featureTagStore.getAllTagsForFeature(featureName)) || - []; + const tags = (await this.featureTagStore.getAllTagsForFeature(featureName)) + || []; await this.eventStore.store({ type: isStale ? FEATURE_STALE_ON : FEATURE_STALE_OFF, @@ -381,8 +373,7 @@ class FeatureToggleServiceV2 { async archiveToggle(name: string, userName: string): Promise { await this.featureToggleStore.hasFeature(name); await this.featureToggleStore.archiveFeature(name); - const tags = - (await this.featureTagStore.getAllTagsForFeature(name)) || []; + const tags = (await this.featureTagStore.getAllTagsForFeature(name)) || []; await this.eventStore.store({ type: FEATURE_ARCHIVED, createdBy: userName, @@ -397,12 +388,12 @@ class FeatureToggleServiceV2 { enabled: boolean, userName: string, ): Promise { - const hasEnvironment = await this.featureStrategiesStore.featureHasEnvironment( + const hasEnvironment = await this.featureEnvironmentStore.featureHasEnvironment( environment, featureName, ); if (hasEnvironment) { - const newEnabled = await this.featureStrategiesStore.toggleEnvironmentEnabledStatus( + const newEnabled = await this.featureEnvironmentStore.toggleEnvironmentEnabledStatus( environment, featureName, enabled, @@ -410,10 +401,9 @@ class FeatureToggleServiceV2 { const feature = await this.featureToggleStore.getFeatureMetadata( featureName, ); - const tags = - (await this.featureTagStore.getAllTagsForFeature( - featureName, - )) || []; + const tags = (await this.featureTagStore.getAllTagsForFeature( + featureName, + )) || []; await this.eventStore.store({ type: FEATURE_UPDATED, createdBy: userName, @@ -434,7 +424,7 @@ class FeatureToggleServiceV2 { userName: string, ): Promise { await this.featureToggleStore.hasFeature(featureName); - const isEnabled = await this.featureStrategiesStore.isEnvironmentEnabled( + const isEnabled = await this.featureEnvironmentStore.isEnvironmentEnabled( featureName, environment, ); @@ -447,10 +437,10 @@ class FeatureToggleServiceV2 { } // @deprecated - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async updateField( featureName: string, field: string, + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types value: any, userName: string, ): Promise { @@ -459,9 +449,8 @@ class FeatureToggleServiceV2 { ); feature[field] = value; await this.featureToggleStore.updateFeature(feature.project, feature); - const tags = - (await this.featureTagStore.getAllTagsForFeature(featureName)) || - []; + const tags = (await this.featureTagStore.getAllTagsForFeature(featureName)) + || []; await this.eventStore.store({ type: FEATURE_UPDATED, @@ -477,14 +466,14 @@ class FeatureToggleServiceV2 { } async deleteFeature(featureName: string, userName: string): Promise { - await this.featureToggleStore.deleteFeature(featureName); + await this.featureToggleStore.delete(featureName); await this.eventStore.store({ type: FEATURE_DELETED, createdBy: userName, data: { - featureName + featureName, }, - }) + }); } async reviveToggle(featureName: string, userName: string): Promise { @@ -494,18 +483,18 @@ class FeatureToggleServiceV2 { type: FEATURE_REVIVED, createdBy: userName, data, - tags + tags, }); - } async getMetadataForAllFeatures(archived: boolean): Promise { return this.featureToggleStore.getFeatures(archived); } - - - + async getProjectId(name: string): Promise { + const { project } = await this.featureToggleStore.getFeatureMetadata(name); + return project; + } } module.exports = FeatureToggleServiceV2; diff --git a/src/lib/services/feature-type-service.ts b/src/lib/services/feature-type-service.ts index 2345ac31c0..39ac51d852 100644 --- a/src/lib/services/feature-type-service.ts +++ b/src/lib/services/feature-type-service.ts @@ -1,10 +1,13 @@ import { IUnleashStores } from '../types/stores'; import { IUnleashConfig } from '../types/option'; -import FeatureTypeStore, { IFeatureType } from '../db/feature-type-store'; import { Logger } from '../logger'; +import { + IFeatureType, + IFeatureTypeStore, +} from '../types/stores/feature-type-store'; export default class FeatureTypeService { - private featureTypeStore: FeatureTypeStore; + private featureTypeStore: IFeatureTypeStore; private logger: Logger; diff --git a/src/lib/services/health-service.ts b/src/lib/services/health-service.ts index 28a62ff887..8842e453d4 100644 --- a/src/lib/services/health-service.ts +++ b/src/lib/services/health-service.ts @@ -1,24 +1,24 @@ -import { Knex } from 'knex'; import { IUnleashStores } from '../types/stores'; import { IUnleashConfig } from '../types/option'; import { Logger } from '../logger'; +import { IFeatureTypeStore } from '../types/stores/feature-type-store'; class HealthService { - private db: Knex; + private featureTypeStore: IFeatureTypeStore; private logger: Logger; constructor( - { db }: Pick, + { featureTypeStore }: Pick, { getLogger }: Pick, ) { - this.db = db; + this.featureTypeStore = featureTypeStore; this.logger = getLogger('services/health-service.ts'); } async dbIsUp(): Promise { - const row = await this.db.raw('select 1'); - return !!row; + const row = await this.featureTypeStore.getAll(); + return row.length > 0; } } diff --git a/src/lib/services/project-health-service.ts b/src/lib/services/project-health-service.ts index 1d9f3b9580..5f63eb2270 100644 --- a/src/lib/services/project-health-service.ts +++ b/src/lib/services/project-health-service.ts @@ -1,6 +1,5 @@ import { IUnleashStores } from '../types/stores'; import { IUnleashConfig } from '../types/option'; -import ProjectStore, { IProject } from '../db/project-store'; import { Logger } from '../logger'; import { FeatureToggle, @@ -12,18 +11,19 @@ import { MILLISECONDS_IN_DAY, MILLISECONDS_IN_ONE_HOUR, } from '../util/constants'; -import FeatureTypeStore from '../db/feature-type-store'; +import { IFeatureToggleStore } from '../types/stores/feature-toggle-store'; +import { IFeatureTypeStore } from '../types/stores/feature-type-store'; +import { IProject, IProjectStore } from '../types/stores/project-store'; import Timer = NodeJS.Timer; -import FeatureToggleStore from '../db/feature-toggle-store'; export default class ProjectHealthService { private logger: Logger; - private projectStore: ProjectStore; + private projectStore: IProjectStore; - private featureTypeStore: FeatureTypeStore; + private featureTypeStore: IFeatureTypeStore; - private featureToggleStore: FeatureToggleStore; + private featureToggleStore: IFeatureToggleStore; private featureTypes: Map; @@ -35,8 +35,8 @@ export default class ProjectHealthService { featureTypeStore, featureToggleStore, }: Pick< - IUnleashStores, - 'projectStore' | 'featureTypeStore' | 'featureToggleStore' + IUnleashStores, + 'projectStore' | 'featureTypeStore' | 'featureToggleStore' >, { getLogger }: Pick, ) { @@ -91,14 +91,14 @@ export default class ProjectHealthService { const today = new Date().valueOf(); if (this.featureTypes.size === 0) { const types = await this.featureTypeStore.getAll(); - types.forEach(type => { + types.forEach((type) => { this.featureTypes.set( type.name.toLowerCase(), type.lifetimeDays, ); }); } - return features.filter(feature => { + return features.filter((feature) => { const diff = today - feature.createdAt.valueOf(); const featureTypeExpectedLifetime = this.featureTypes.get( feature.type, @@ -111,11 +111,11 @@ export default class ProjectHealthService { } private activeCount(features: IFeatureOverview[]): number { - return features.filter(f => !f.stale).length; + return features.filter((f) => !f.stale).length; } private staleCount(features: IFeatureOverview[]): number { - return features.filter(f => f.stale).length; + return features.filter((f) => f.stale).length; } async calculateHealthRating(project: IProject): Promise { @@ -123,7 +123,7 @@ export default class ProjectHealthService { project: project.id, }); - const activeToggles = toggles.filter(feature => !feature.stale); + const activeToggles = toggles.filter((feature) => !feature.stale); const staleToggles = toggles.length - activeToggles.length; const potentiallyStaleToggles = await this.potentiallyStaleCount( activeToggles, @@ -154,7 +154,7 @@ export default class ProjectHealthService { const projects = await this.projectStore.getAll(); await Promise.all( - projects.map(async project => { + projects.map(async (project) => { const newHealth = await this.calculateHealthRating(project); await this.projectStore.updateHealth({ id: project.id, diff --git a/src/lib/services/project-schema.js b/src/lib/services/project-schema.js index 9fd1e9f426..d1ba202eca 100644 --- a/src/lib/services/project-schema.js +++ b/src/lib/services/project-schema.js @@ -6,11 +6,7 @@ const projectSchema = joi .keys({ id: nameType, name: joi.string().required(), - description: joi - .string() - .allow(null) - .allow('') - .optional(), + description: joi.string().allow(null).allow('').optional(), }) .options({ allowUnknown: false, stripUnknown: true }); diff --git a/src/lib/services/project-service.ts b/src/lib/services/project-service.ts index 04a3740a05..334938c101 100644 --- a/src/lib/services/project-service.ts +++ b/src/lib/services/project-service.ts @@ -1,14 +1,10 @@ import User from '../types/user'; -import { AccessService, IUserWithRole, RoleName } from './access-service'; -import ProjectStore, { IProject } from '../db/project-store'; -import EventStore from '../db/event-store'; +import { AccessService } from './access-service'; import NameExistsError from '../error/name-exists-error'; import InvalidOperationError from '../error/invalid-operation-error'; import { nameType } from '../routes/admin-api/util'; import schema from './project-schema'; import NotFoundError from '../error/notfound-error'; -import FeatureToggleStore from '../db/feature-toggle-store'; -import { IRole } from '../db/access-store'; import { PROJECT_CREATED, PROJECT_DELETED, @@ -16,19 +12,14 @@ import { } from '../types/events'; import { IUnleashStores } from '../types/stores'; import { IUnleashConfig } from '../types/option'; -import FeatureTypeStore from '../db/feature-type-store'; -import { - FeatureToggle, - IProjectHealthReport, - IProjectOverview, -} from '../types/model'; -import Timer = NodeJS.Timer; -import { - MILLISECONDS_IN_DAY, - MILLISECONDS_IN_ONE_HOUR, -} from '../util/constants'; -import EnvironmentStore from '../db/environment-store'; +import { IProjectOverview, IUserWithRole, RoleName } from '../types/model'; import { GLOBAL_ENV } from '../types/environment'; +import { IEnvironmentStore } from '../types/stores/environment-store'; +import { IFeatureTypeStore } from '../types/stores/feature-type-store'; +import { IFeatureToggleStore } from '../types/stores/feature-toggle-store'; +import { IProject, IProjectStore } from '../types/stores/project-store'; +import { IRole } from '../types/stores/access-store'; +import { IEventStore } from '../types/stores/event-store'; const getCreatedBy = (user: User) => user.email || user.username; @@ -40,17 +31,17 @@ export interface UsersWithRoles { } export default class ProjectService { - private projectStore: ProjectStore; + private projectStore: IProjectStore; private accessService: AccessService; - private eventStore: EventStore; + private eventStore: IEventStore; - private featureToggleStore: FeatureToggleStore; + private featureToggleStore: IFeatureToggleStore; - private featureTypeStore: FeatureTypeStore; + private featureTypeStore: IFeatureTypeStore; - private environmentStore: EnvironmentStore; + private environmentStore: IEnvironmentStore; private logger: any; @@ -157,15 +148,10 @@ export default class ProjectService { } async validateUniqueId(id: string): Promise { - try { - await this.projectStore.hasProject(id); - } catch (error) { - // No conflict, everything ok! - return; + const exists = await this.projectStore.hasProject(id); + if (exists) { + throw new NameExistsError('A project with this id already exists.'); } - - // Intentional throw here! - throw new NameExistsError('A project with this id already exists.'); } // RBAC methods @@ -189,14 +175,14 @@ export default class ProjectService { projectId, ); - const role = roles.find(r => r.id === roleId); + const role = roles.find((r) => r.id === roleId); if (!role) { throw new NotFoundError( `Could not find roleId=${roleId} on project=${projectId}`, ); } - const alreadyHasAccess = users.some(u => u.id === userId); + const alreadyHasAccess = users.some((u) => u.id === userId); if (alreadyHasAccess) { throw new Error(`User already have access to project=${projectId}`); } @@ -210,7 +196,7 @@ export default class ProjectService { userId: number, ): Promise { const roles = await this.accessService.getRolesForProject(projectId); - const role = roles.find(r => r.id === roleId); + const role = roles.find((r) => r.id === roleId); if (!role) { throw new NotFoundError( `Couldn't find roleId=${roleId} on project=${projectId}`, diff --git a/src/lib/services/reset-token-service.ts b/src/lib/services/reset-token-service.ts index f7f4392579..3a272fa590 100644 --- a/src/lib/services/reset-token-service.ts +++ b/src/lib/services/reset-token-service.ts @@ -1,40 +1,35 @@ import crypto from 'crypto'; import bcrypt from 'bcrypt'; import { URL } from 'url'; -import { - ResetTokenStore, - IResetToken, - IResetQuery, -} from '../db/reset-token-store'; import { Logger } from '../logger'; -import UserStore from '../db/user-store'; import UsedTokenError from '../error/used-token-error'; import InvalidTokenError from '../error/invalid-token-error'; import { IUnleashConfig } from '../types/option'; +import { IUnleashStores } from '../types/stores'; +import { + IResetQuery, + IResetToken, + IResetTokenStore, +} from '../types/stores/reset-token-store'; const ONE_DAY = 86_400_000; -interface IStores { - resetTokenStore: ResetTokenStore; - userStore: UserStore; -} - interface IInviteLinks { [key: string]: string; } export default class ResetTokenService { - private store: ResetTokenStore; + private store: IResetTokenStore; private logger: Logger; private readonly unleashBase: string; constructor( - stores: IStores, + { resetTokenStore }: Pick, { getLogger, server }: Pick, ) { - this.store = stores.resetTokenStore; + this.store = resetTokenStore; this.logger = getLogger('/services/reset-token-service.ts'); this.unleashBase = server.unleashUrl; } @@ -53,9 +48,8 @@ export default class ResetTokenService { try { const tokens = await this.store.getActiveTokens(); const links = tokens.reduce((acc, token) => { - const inviteLink = this.getExistingInvitationUrl( - token, - ).toString(); + const inviteLink = + this.getExistingInvitationUrl(token).toString(); acc[token.userId] = inviteLink; diff --git a/src/lib/services/session-service.ts b/src/lib/services/session-service.ts index fab8cce7c6..b52068fb49 100644 --- a/src/lib/services/session-service.ts +++ b/src/lib/services/session-service.ts @@ -1,12 +1,12 @@ import { IUnleashStores } from '../types/stores'; import { IUnleashConfig } from '../types/option'; import { Logger } from '../logger'; -import SessionStore, { ISession } from '../db/session-store'; +import { ISession, ISessionStore } from '../types/stores/session-store'; export default class SessionService { private logger: Logger; - private sessionStore: SessionStore; + private sessionStore: ISessionStore; constructor( { sessionStore }: Pick, @@ -25,7 +25,7 @@ export default class SessionService { } async getSession(sid: string): Promise { - return this.sessionStore.getSession(sid); + return this.sessionStore.get(sid); } async deleteSessionsForUser(userId: number): Promise { @@ -33,7 +33,7 @@ export default class SessionService { } async deleteSession(sid: string): Promise { - return this.sessionStore.deleteSession(sid); + return this.sessionStore.delete(sid); } async insertSession({ diff --git a/src/lib/services/setting-service.ts b/src/lib/services/setting-service.ts index 266adc352f..4d7a8d3b0e 100644 --- a/src/lib/services/setting-service.ts +++ b/src/lib/services/setting-service.ts @@ -1,12 +1,12 @@ import { IUnleashConfig } from '../types/option'; import { IUnleashStores } from '../types/stores'; import { Logger } from '../logger'; -import SettingStore from '../db/setting-store'; +import { ISettingStore } from '../types/stores/settings-store'; export default class SettingService { private logger: Logger; - private settingStore: SettingStore; + private settingStore: ISettingStore; constructor( { settingStore }: Pick, @@ -23,6 +23,10 @@ export default class SettingService { async insert(id: string, value: object): Promise { return this.settingStore.insert(id, value); } + + async delete(id: string): Promise { + return this.settingStore.delete(id); + } } module.exports = SettingService; diff --git a/src/lib/services/state-schema.ts b/src/lib/services/state-schema.ts index 24a20eb1d9..4ca6b018c2 100644 --- a/src/lib/services/state-schema.ts +++ b/src/lib/services/state-schema.ts @@ -21,54 +21,27 @@ export const featureStrategySchema = joi export const featureEnvironmentsSchema = joi.object().keys({ environment: joi.string(), - feature_name: joi.string(), + featureName: joi.string(), enabled: joi.boolean(), }); export const environmentSchema = joi.object().keys({ name: nameType, - displayName: joi - .string() - .optional() - .allow(''), + displayName: joi.string().optional().allow(''), }); export const stateSchema = joi.object().keys({ version: joi.number(), - features: joi - .array() - .optional() - .items(featureSchema), - strategies: joi - .array() - .optional() - .items(strategySchema), - tags: joi - .array() - .optional() - .items(tagSchema), - tagTypes: joi - .array() - .optional() - .items(tagTypeSchema), - featureTags: joi - .array() - .optional() - .items(featureTagSchema), - projects: joi - .array() - .optional() - .items(projectSchema), - featureStrategies: joi - .array() - .optional() - .items(featureStrategySchema), + features: joi.array().optional().items(featureSchema), + strategies: joi.array().optional().items(strategySchema), + tags: joi.array().optional().items(tagSchema), + tagTypes: joi.array().optional().items(tagTypeSchema), + featureTags: joi.array().optional().items(featureTagSchema), + projects: joi.array().optional().items(projectSchema), + featureStrategies: joi.array().optional().items(featureStrategySchema), featureEnvironments: joi .array() .optional() .items(featureEnvironmentsSchema), - environments: joi - .array() - .optional() - .items(environmentSchema), + environments: joi.array().optional().items(environmentSchema), }); diff --git a/src/lib/services/state-service.test.js b/src/lib/services/state-service.test.ts similarity index 94% rename from src/lib/services/state-service.test.js rename to src/lib/services/state-service.test.ts index 87df55f602..d3241b09c8 100644 --- a/src/lib/services/state-service.test.js +++ b/src/lib/services/state-service.test.ts @@ -1,11 +1,8 @@ -'use strict'; +import createStores from '../../test/fixtures/store'; +import getLogger from '../../test/fixtures/no-logger'; -const store = require('../../test/fixtures/store'); -const getLogger = require('../../test/fixtures/no-logger'); - -const StateService = require('./state-service'); -const NotFoundError = require('../error/notfound-error'); -const { +import StateService from './state-service'; +import { FEATURE_IMPORT, DROP_FEATURES, STRATEGY_IMPORT, @@ -13,12 +10,12 @@ const { TAG_TYPE_IMPORT, TAG_IMPORT, PROJECT_IMPORT, -} = require('../types/events'); +} from '../types/events'; const oldExportExample = require('./state-service-export-v1.json'); function getSetup() { - const stores = store.createStores(); + const stores = createStores(); return { stateService: new StateService(stores, { getLogger }), stores, @@ -209,7 +206,9 @@ test('should not accept gibberish', async () => { test('should export featureToggles', async () => { const { stateService, stores } = getSetup(); - stores.featureToggleStore.createFeature('default', { name: 'a-feature' }); + await stores.featureToggleStore.createFeature('default', { + name: 'a-feature', + }); const data = await stateService.export({ includeFeatureToggles: true }); @@ -220,7 +219,10 @@ test('should export featureToggles', async () => { test('should export strategies', async () => { const { stateService, stores } = getSetup(); - stores.strategyStore.createStrategy({ name: 'a-strategy', editable: true }); + await stores.strategyStore.createStrategy({ + name: 'a-strategy', + editable: true, + }); const data = await stateService.export({ includeStrategies: true }); @@ -306,7 +308,7 @@ test('Should not keep existing tags if drop-before-import', async () => { expect(tagTypes).toHaveLength(1); }); -test('should export tag, tagtypes', async () => { +test('should export tag, tagtypes but not feature tags if the feature is not exported', async () => { const { stateService, stores } = getSetup(); const data = { @@ -448,9 +450,8 @@ test('Should drop projects before import if specified', async () => { description: 'Not expected to be seen after import', }); await stateService.import({ data, dropBeforeImport: true }); - return expect(async () => - stores.projectStore.hasProject('fancy'), - ).rejects.toThrow(NotFoundError); + const hasProject = await stores.projectStore.hasProject('fancy'); + expect(hasProject).toBe(false); }); test('Should export projects', async () => { @@ -490,7 +491,7 @@ test('exporting to new format works', async () => { name: 'Some-feature', }); await stores.strategyStore.createStrategy({ name: 'format' }); - await stores.featureStrategiesStore.connectEnvironmentAndFeature( + await stores.featureEnvironmentStore.connectEnvironmentAndFeature( 'Some-feature', 'dev', true, @@ -530,7 +531,7 @@ test('featureStrategies can keep existing', async () => { name: 'Some-feature', }); await stores.strategyStore.createStrategy({ name: 'format' }); - await stores.featureStrategiesStore.connectEnvironmentAndFeature( + await stores.featureEnvironmentStore.connectEnvironmentAndFeature( 'Some-feature', 'dev', true, @@ -576,7 +577,7 @@ test('featureStrategies should not keep existing if dropBeforeImport', async () name: 'Some-feature', }); await stores.strategyStore.createStrategy({ name: 'format' }); - await stores.featureStrategiesStore.connectEnvironmentAndFeature( + await stores.featureEnvironmentStore.connectEnvironmentAndFeature( 'Some-feature', 'dev', true, @@ -618,8 +619,8 @@ test('Import v1 and exporting v2 should work', async () => { 0, ); expect( - exported.features.every(f => - oldExportExample.features.some(old => old.name === f.name), + exported.features.every((f) => + oldExportExample.features.some((old) => old.name === f.name), ), ).toBeTruthy(); expect(exported.featureStrategies).toHaveLength(strategiesCount); diff --git a/src/lib/services/state-service.ts b/src/lib/services/state-service.ts index 915b154d0e..debab70c42 100644 --- a/src/lib/services/state-service.ts +++ b/src/lib/services/state-service.ts @@ -15,27 +15,33 @@ import { } from '../types/events'; import { filterEqual, filterExisting, parseFile, readFile } from './state-util'; -import FeatureToggleStore from '../db/feature-toggle-store'; -import TagTypeStore, { ITagType } from '../db/tag-type-store'; -import FeatureTagStore, { IFeatureTag } from '../db/feature-tag-store'; -import ProjectStore, { IProject } from '../db/project-store'; -import TagStore from '../db/tag-store'; -import StrategyStore, { IStrategy } from '../db/strategy-store'; -import { Logger } from '../logger'; -import { IUnleashStores } from '../types/stores'; + import { IUnleashConfig } from '../types/option'; -import EventStore from '../db/event-store'; import { FeatureToggle, IEnvironment, + IImportFile, IFeatureEnvironment, - ITag, -} from '../types/model'; -import FeatureStrategiesStore, { IFeatureStrategy, -} from '../db/feature-strategy-store'; -import EnvironmentStore from '../db/environment-store'; + ITag, + IImportData, +} from '../types/model'; import { GLOBAL_ENV } from '../types/environment'; +import { Logger } from '../logger'; +import { + IFeatureTag, + IFeatureTagStore, +} from '../types/stores/feature-tag-store'; +import { IProject, IProjectStore } from '../types/stores/project-store'; +import { ITagType, ITagTypeStore } from '../types/stores/tag-type-store'; +import { ITagStore } from '../types/stores/tag-store'; +import { IEventStore } from '../types/stores/event-store'; +import { IStrategy, IStrategyStore } from '../types/stores/strategy-store'; +import { IFeatureToggleStore } from '../types/stores/feature-toggle-store'; +import { IFeatureStrategiesStore } from '../types/stores/feature-strategies-store'; +import { IEnvironmentStore } from '../types/stores/environment-store'; +import { IFeatureEnvironmentStore } from '../types/stores/feature-environment-store'; +import { IUnleashStores } from '../types/stores'; export interface IBackupOption { includeFeatureToggles: boolean; @@ -44,32 +50,36 @@ export interface IBackupOption { includeTags: boolean; } -interface IImportOption { - keepExising: boolean; - dropBeforeImport: boolean; - userName: string; +interface IExportIncludeOptions { + includeFeatureToggles?: boolean; + includeStrategies?: boolean; + includeProjects?: boolean; + includeTags?: boolean; + includeEnvironments?: boolean; } export default class StateService { private logger: Logger; - private toggleStore: FeatureToggleStore; + private toggleStore: IFeatureToggleStore; - private featureStrategiesStore: FeatureStrategiesStore; + private featureStrategiesStore: IFeatureStrategiesStore; - private strategyStore: StrategyStore; + private strategyStore: IStrategyStore; - private eventStore: EventStore; + private eventStore: IEventStore; - private tagStore: TagStore; + private tagStore: ITagStore; - private tagTypeStore: TagTypeStore; + private tagTypeStore: ITagTypeStore; - private projectStore: ProjectStore; + private projectStore: IProjectStore; - private featureTagStore: FeatureTagStore; + private featureEnvironmentStore: IFeatureEnvironmentStore; - private environmentStore: EnvironmentStore; + private featureTagStore: IFeatureTagStore; + + private environmentStore: IEnvironmentStore; constructor( stores: IUnleashStores, @@ -80,6 +90,7 @@ export default class StateService { this.strategyStore = stores.strategyStore; this.tagStore = stores.tagStore; this.featureStrategiesStore = stores.featureStrategiesStore; + this.featureEnvironmentStore = stores.featureEnvironmentStore; this.tagTypeStore = stores.tagTypeStore; this.projectStore = stores.projectStore; this.featureTagStore = stores.featureTagStore; @@ -89,23 +100,28 @@ export default class StateService { async importFile({ file, - dropBeforeImport, - userName, - keepExisting, - }): Promise { + dropBeforeImport = false, + userName = 'import-user', + keepExisting = true, + }: IImportFile): Promise { return readFile(file) - .then(data => parseFile(file, data)) - .then(data => - this.import({ data, userName, dropBeforeImport, keepExisting }), + .then((data) => parseFile(file, data)) + .then((data) => + this.import({ + data, + userName, + dropBeforeImport, + keepExisting, + }), ); } async import({ data, - userName, - dropBeforeImport, - keepExisting, - }): Promise { + userName = 'importUser', + dropBeforeImport = false, + keepExisting = true, + }: IImportData): Promise { const importData = await stateSchema.validateAsync(data); if (importData.features) { @@ -115,11 +131,8 @@ export default class StateService { } else { projectData = importData; } - const { - features, - featureStrategies, - featureEnvironments, - } = projectData; + const { features, featureStrategies, featureEnvironments } = + projectData; await this.importFeatures({ features, @@ -132,7 +145,6 @@ export default class StateService { }); await this.importFeatureStrategies({ featureStrategies, - userName, dropBeforeImport, keepExisting, }); @@ -162,12 +174,12 @@ export default class StateService { tags: data.tags, featureTags: (data.featureTags || []) - .filter(t => + .filter((t) => (data.features || []).some( - f => f.name === t.featureName, + (f) => f.name === t.featureName, ), ) - .map(t => ({ + .map((t) => ({ featureName: t.featureName, tagValue: t.tagValue || t.value, tagType: t.tagType || t.type, @@ -179,10 +191,11 @@ export default class StateService { } } + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async importFeatureEnvironments({ featureEnvironments }): Promise { await Promise.all( - featureEnvironments.map(env => - this.featureStrategiesStore.connectEnvironmentAndFeature( + featureEnvironments.map((env) => + this.featureEnvironmentStore.connectEnvironmentAndFeature( env.featureName, env.environment, env.enabled, @@ -191,9 +204,9 @@ export default class StateService { ); } + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async importFeatureStrategies({ featureStrategies, - userName, dropBeforeImport, keepExisting, }): Promise { @@ -204,15 +217,15 @@ export default class StateService { this.logger.info( 'Dropping existing strategies for feature toggles', ); - await this.featureStrategiesStore.deleteFeatureStrategies(); + await this.featureStrategiesStore.deleteAll(); } const strategiesToImport = keepExisting ? featureStrategies.filter( - s => !oldFeatureStrategies.some(o => o.id === s.id), - ) + (s) => !oldFeatureStrategies.some((o) => o.id === s.id), + ) : featureStrategies; await Promise.all( - strategiesToImport.map(featureStrategy => + strategiesToImport.map((featureStrategy) => this.featureStrategiesStore.createStrategyConfig( featureStrategy, ), @@ -220,11 +233,12 @@ export default class StateService { ); } + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async convertLegacyFeatures({ features, }): Promise<{ features; featureStrategies; featureEnvironments }> { - const strategies = features.flatMap(f => - f.strategies.map(strategy => ({ + const strategies = features.flatMap((f) => + f.strategies.map((strategy) => ({ featureName: f.name, projectName: f.project, constraints: strategy.constraints || [], @@ -234,7 +248,7 @@ export default class StateService { })), ); const newFeatures = features; - const featureEnvironments = features.map(feature => ({ + const featureEnvironments = features.map((feature) => ({ featureName: feature.name, environment: GLOBAL_ENV, enabled: feature.enabled, @@ -246,6 +260,7 @@ export default class StateService { }; } + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async importFeatures({ features, userName, @@ -255,11 +270,11 @@ export default class StateService { this.logger.info(`Importing ${features.length} feature toggles`); const oldToggles = dropBeforeImport ? [] - : await this.toggleStore.getFeatures(); + : await this.toggleStore.getAll(); if (dropBeforeImport) { this.logger.info('Dropping existing feature toggles'); - await this.toggleStore.dropFeatures(); + await this.toggleStore.deleteAll(); await this.eventStore.store({ type: DROP_FEATURES, createdBy: userName, @@ -271,7 +286,7 @@ export default class StateService { features .filter(filterExisting(keepExisting, oldToggles)) .filter(filterEqual(oldToggles)) - .map(feature => + .map((feature) => this.toggleStore .createFeature(feature.project, feature) .then(() => { @@ -285,6 +300,7 @@ export default class StateService { ); } + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async importStrategies({ strategies, userName, @@ -294,11 +310,11 @@ export default class StateService { this.logger.info(`Importing ${strategies.length} strategies`); const oldStrategies = dropBeforeImport ? [] - : await this.strategyStore.getStrategies(); + : await this.strategyStore.getAll(); if (dropBeforeImport) { this.logger.info('Dropping existing strategies'); - await this.strategyStore.dropStrategies(); + await this.strategyStore.deleteAll(); await this.eventStore.store({ type: DROP_STRATEGIES, createdBy: userName, @@ -310,7 +326,7 @@ export default class StateService { strategies .filter(filterExisting(keepExisting, oldStrategies)) .filter(filterEqual(oldStrategies)) - .map(strategy => + .map((strategy) => this.strategyStore.importStrategy(strategy).then(() => { this.eventStore.store({ type: STRATEGY_IMPORT, @@ -322,6 +338,7 @@ export default class StateService { ); } + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async importProjects({ projects, userName, @@ -334,23 +351,23 @@ export default class StateService { : await this.projectStore.getAll(); if (dropBeforeImport) { this.logger.info('Dropping existing projects'); - await this.projectStore.dropProjects(); + await this.projectStore.deleteAll(); await this.eventStore.store({ type: DROP_PROJECTS, createdBy: userName, data: { name: 'all-projects' }, }); } - const projectsToImport = projects.filter(project => + const projectsToImport = projects.filter((project) => keepExisting - ? !oldProjects.some(old => old.id === project.id) + ? !oldProjects.some((old) => old.id === project.id) : true, ); if (projectsToImport.length > 0) { const importedProjects = await this.projectStore.importProjects( projectsToImport, ); - const importedProjectEvents = importedProjects.map(project => ({ + const importedProjectEvents = importedProjects.map((project) => ({ type: PROJECT_IMPORT, createdBy: userName, data: project, @@ -359,6 +376,7 @@ export default class StateService { } } + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async importTagData({ tagTypes, tags, @@ -376,14 +394,14 @@ export default class StateService { const oldTags = dropBeforeImport ? [] : await this.tagStore.getAll(); const oldFeatureTags = dropBeforeImport ? [] - : await this.featureTagStore.getAllFeatureTags(); + : await this.featureTagStore.getAll(); if (dropBeforeImport) { this.logger.info( 'Dropping all existing featuretags, tags and tagtypes', ); - await this.featureTagStore.dropFeatureTags(); - await this.tagStore.dropTags(); - await this.tagTypeStore.dropTagTypes(); + await this.featureTagStore.deleteAll(); + await this.tagStore.deleteAll(); + await this.tagTypeStore.deleteAll(); await this.eventStore.batchStore([ { type: DROP_FEATURE_TAGS, @@ -431,16 +449,19 @@ export default class StateService { oldFeatureTags: IFeatureTag[], userName: string, ): Promise { - const featureTagsToInsert = featureTags.filter(tag => + const featureTagsToInsert = featureTags.filter((tag) => keepExisting - ? !oldFeatureTags.some(old => this.compareFeatureTags(old, tag)) + ? !oldFeatureTags.some((old) => + this.compareFeatureTags(old, tag), + ) : true, ); if (featureTagsToInsert.length > 0) { - const importedFeatureTags = await this.featureTagStore.importFeatureTags( - featureTagsToInsert, - ); - const importedFeatureTagEvents = importedFeatureTags.map(tag => ({ + const importedFeatureTags = + await this.featureTagStore.importFeatureTags( + featureTagsToInsert, + ); + const importedFeatureTagEvents = importedFeatureTags.map((tag) => ({ type: FEATURE_TAG_IMPORT, createdBy: userName, data: tag, @@ -458,14 +479,14 @@ export default class StateService { oldTags: ITag[], userName: string, ): Promise { - const tagsToInsert = tags.filter(tag => + const tagsToInsert = tags.filter((tag) => keepExisting - ? !oldTags.some(old => this.compareTags(old, tag)) + ? !oldTags.some((old) => this.compareTags(old, tag)) : true, ); if (tagsToInsert.length > 0) { const importedTags = await this.tagStore.bulkImport(tagsToInsert); - const importedTagEvents = importedTags.map(tag => ({ + const importedTagEvents = importedTags.map((tag) => ({ type: TAG_IMPORT, createdBy: userName, data: tag, @@ -480,16 +501,16 @@ export default class StateService { oldTagTypes: ITagType[] = [], userName: string, ): Promise { - const tagTypesToInsert = tagTypes.filter(tagType => + const tagTypesToInsert = tagTypes.filter((tagType) => keepExisting - ? !oldTagTypes.some(t => t.name === tagType.name) + ? !oldTagTypes.some((t) => t.name === tagType.name) : true, ); if (tagTypesToInsert.length > 0) { const importedTagTypes = await this.tagTypeStore.bulkImport( tagTypesToInsert, ); - const importedTagTypeEvents = importedTagTypes.map(tagType => ({ + const importedTagTypeEvents = importedTagTypes.map((tagType) => ({ type: TAG_TYPE_IMPORT, createdBy: userName, data: tagType, @@ -498,27 +519,28 @@ export default class StateService { } } + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async export({ includeFeatureToggles = true, includeStrategies = true, includeProjects = true, includeTags = true, includeEnvironments = true, - }): Promise<{ - features: FeatureToggle[]; - strategies: IStrategy[]; - version: number; - projects: IProject[]; - tagTypes: ITagType[]; - tags: ITag[]; - featureTags: IFeatureTag[]; - featureStrategies: IFeatureStrategy[]; - environments: IEnvironment[]; - featureEnvironments: IFeatureEnvironment[]; - }> { + }: IExportIncludeOptions): Promise<{ + features: FeatureToggle[]; + strategies: IStrategy[]; + version: number; + projects: IProject[]; + tagTypes: ITagType[]; + tags: ITag[]; + featureTags: IFeatureTag[]; + featureStrategies: IFeatureStrategy[]; + environments: IEnvironment[]; + featureEnvironments: IFeatureEnvironment[]; + }> { return Promise.all([ includeFeatureToggles - ? this.toggleStore.getFeatures() + ? this.toggleStore.getAll() : Promise.resolve([]), includeStrategies ? this.strategyStore.getEditableStrategies() @@ -529,7 +551,7 @@ export default class StateService { includeTags ? this.tagTypeStore.getAll() : Promise.resolve([]), includeTags ? this.tagStore.getAll() : Promise.resolve([]), includeTags && includeFeatureToggles - ? this.featureTagStore.getAllFeatureTags() + ? this.featureTagStore.getAll() : Promise.resolve([]), includeFeatureToggles ? this.featureStrategiesStore.getAll() @@ -538,7 +560,7 @@ export default class StateService { ? this.environmentStore.getAll() : Promise.resolve([]), includeFeatureToggles - ? this.featureStrategiesStore.getAllFeatureEnvironments() + ? this.featureEnvironmentStore.getAll() : Promise.resolve([]), ]).then( ([ diff --git a/src/lib/services/state-util.js b/src/lib/services/state-util.js deleted file mode 100644 index 2b7bec7d22..0000000000 --- a/src/lib/services/state-util.js +++ /dev/null @@ -1,34 +0,0 @@ -const fs = require('fs'); -const mime = require('mime'); -const YAML = require('js-yaml'); - -const readFile = file => - new Promise((resolve, reject) => - fs.readFile(file, (err, v) => (err ? reject(err) : resolve(v))), - ); - -const parseFile = (file, data) => - mime.getType(file) === 'text/yaml' ? YAML.safeLoad(data) : JSON.parse(data); - -const filterExisting = (keepExisting, existingArray = []) => item => { - if (keepExisting) { - const found = existingArray.find(t => t.name === item.name); - return !found; - } - return true; -}; - -const filterEqual = (existingArray = []) => item => { - const toggle = existingArray.find(t => t.name === item.name); - if (toggle) { - return JSON.stringify(toggle) !== JSON.stringify(item); - } - return true; -}; - -module.exports = { - readFile, - parseFile, - filterExisting, - filterEqual, -}; diff --git a/src/lib/services/state-util.ts b/src/lib/services/state-util.ts new file mode 100644 index 0000000000..a576781ab1 --- /dev/null +++ b/src/lib/services/state-util.ts @@ -0,0 +1,39 @@ +import * as fs from 'fs'; +import * as mime from 'mime'; +import * as YAML from 'js-yaml'; + +export const readFile: (file: string) => Promise = (file) => + new Promise((resolve, reject) => + fs.readFile(file, (err, v) => + err ? reject(err) : resolve(v.toString('utf-8')), + ), + ); + +export const parseFile: (file: string, data: string) => any = ( + file: string, + data: string, +) => + mime.lookup(file) === 'text/yaml' ? YAML.safeLoad(data) : JSON.parse(data); + +export const filterExisting: ( + keepExisting: boolean, + existingArray: any[], +) => (item: any) => boolean = + (keepExisting, existingArray = []) => + (item) => { + if (keepExisting) { + const found = existingArray.find((t) => t.name === item.name); + return !found; + } + return true; + }; + +export const filterEqual: (existingArray: any[]) => (item: any) => boolean = + (existingArray = []) => + (item) => { + const toggle = existingArray.find((t) => t.name === item.name); + if (toggle) { + return JSON.stringify(toggle) !== JSON.stringify(item); + } + return true; + }; diff --git a/src/lib/services/strategy-schema.ts b/src/lib/services/strategy-schema.ts index 6f5850c44b..6e24cc8e4a 100644 --- a/src/lib/services/strategy-schema.ts +++ b/src/lib/services/strategy-schema.ts @@ -1,5 +1,3 @@ -'use strict'; - const joi = require('joi'); const { nameType } = require('../routes/admin-api/util'); @@ -9,11 +7,7 @@ const strategySchema = joi name: nameType, editable: joi.boolean().default(true), deprecated: joi.boolean().default(false), - description: joi - .string() - .allow(null) - .allow('') - .optional(), + description: joi.string().allow(null).allow('').optional(), parameters: joi .array() .required() @@ -21,11 +15,7 @@ const strategySchema = joi joi.object().keys({ name: joi.string().required(), type: joi.string().required(), - description: joi - .string() - .allow(null) - .allow('') - .optional(), + description: joi.string().allow(null).allow('').optional(), required: joi.boolean(), }), ), diff --git a/src/lib/services/strategy-service.ts b/src/lib/services/strategy-service.ts index 9f28e8600a..26343aff42 100644 --- a/src/lib/services/strategy-service.ts +++ b/src/lib/services/strategy-service.ts @@ -1,8 +1,13 @@ import { Logger } from '../logger'; -import EventStore from '../db/event-store'; -import StrategyStore, { IStrategy } from '../db/strategy-store'; import { IUnleashConfig } from '../types/option'; import { IUnleashStores } from '../types/stores'; +import { IEventStore } from '../types/stores/event-store'; +import { + IMinimalStrategy, + IStrategy, + IStrategyStore, +} from '../types/stores/strategy-store'; +import NotFoundError from '../error/notfound-error'; const strategySchema = require('./strategy-schema'); const NameExistsError = require('../error/name-exists-error'); @@ -17,9 +22,9 @@ const { class StrategyService { private logger: Logger; - private strategyStore: StrategyStore; + private strategyStore: IStrategyStore; - private eventStore: EventStore; + private eventStore: IEventStore; constructor( { @@ -34,20 +39,20 @@ class StrategyService { } async getStrategies(): Promise { - return this.strategyStore.getStrategies(); + return this.strategyStore.getAll(); } async getStrategy(name: string): Promise { - return this.strategyStore.getStrategy(name); + return this.strategyStore.get(name); } async removeStrategy( strategyName: string, userName: string, ): Promise { - const strategy = await this.strategyStore.getStrategy(strategyName); + const strategy = await this.strategyStore.get(strategyName); await this._validateEditable(strategy); - await this.strategyStore.deleteStrategy({ name: strategyName }); + await this.strategyStore.delete(strategyName); await this.eventStore.store({ type: STRATEGY_DELETED, createdBy: userName, @@ -61,22 +66,28 @@ class StrategyService { strategyName: string, userName: string, ): Promise { - await this.strategyStore.getStrategy(strategyName); // Check existence - await this.strategyStore.deprecateStrategy({ name: strategyName }); - await this.eventStore.store({ - type: STRATEGY_DEPRECATED, - createdBy: userName, - data: { - name: strategyName, - }, - }); + if (await this.strategyStore.exists(strategyName)) { + // Check existence + await this.strategyStore.deprecateStrategy({ name: strategyName }); + await this.eventStore.store({ + type: STRATEGY_DEPRECATED, + createdBy: userName, + data: { + name: strategyName, + }, + }); + } else { + throw new NotFoundError( + `Could not find strategy with name ${strategyName}`, + ); + } } async reactivateStrategy( strategyName: string, userName: string, ): Promise { - await this.strategyStore.getStrategy(strategyName); // Check existence + await this.strategyStore.get(strategyName); // Check existence await this.strategyStore.reactivateStrategy({ name: strategyName }); await this.eventStore.store({ type: STRATEGY_REACTIVATED, @@ -87,7 +98,10 @@ class StrategyService { }); } - async createStrategy(value, userName: string): Promise { + async createStrategy( + value: IMinimalStrategy, + userName: string, + ): Promise { const strategy = await strategySchema.validateAsync(value); strategy.deprecated = false; await this._validateStrategyName(strategy); @@ -99,9 +113,12 @@ class StrategyService { }); } - async updateStrategy(input, userName: string): Promise { + async updateStrategy( + input: IMinimalStrategy, + userName: string, + ): Promise { const value = await strategySchema.validateAsync(input); - const strategy = await this.strategyStore.getStrategy(input.name); + const strategy = await this.strategyStore.get(input.name); await this._validateEditable(strategy); await this.strategyStore.updateStrategy(value); await this.eventStore.store({ @@ -116,7 +133,7 @@ class StrategyService { ): Promise> { return new Promise((resolve, reject) => { this.strategyStore - .getStrategy(data.name) + .get(data.name) .then(() => reject( new NameExistsError( diff --git a/src/lib/services/tag-schema.test.js b/src/lib/services/tag-schema.test.ts similarity index 81% rename from src/lib/services/tag-schema.test.js rename to src/lib/services/tag-schema.test.ts index ce25beedc1..1cbd88a19b 100644 --- a/src/lib/services/tag-schema.test.js +++ b/src/lib/services/tag-schema.test.ts @@ -1,6 +1,4 @@ -'use strict'; - -const { tagSchema } = require('./tag-schema'); +import { tagSchema } from './tag-schema'; test('should require url friendly type if defined', () => { const tag = { diff --git a/src/lib/services/tag-schema.ts b/src/lib/services/tag-schema.ts index 8606ef9fc8..2dc20291f5 100644 --- a/src/lib/services/tag-schema.ts +++ b/src/lib/services/tag-schema.ts @@ -4,14 +4,8 @@ import { customJoi } from '../routes/admin-api/util'; export const tagSchema = Joi.object() .keys({ - value: Joi.string() - .min(2) - .max(50), - type: customJoi - .isUrlFriendly() - .min(2) - .max(50) - .default('simple'), + value: Joi.string().min(2).max(50), + type: customJoi.isUrlFriendly().min(2).max(50).default('simple'), }) .options({ allowUnknown: false, diff --git a/src/lib/services/tag-service.ts b/src/lib/services/tag-service.ts index 89a11e5a98..8bed11b3f8 100644 --- a/src/lib/services/tag-service.ts +++ b/src/lib/services/tag-service.ts @@ -1,18 +1,27 @@ import { tagSchema } from './tag-schema'; -import TagStore, { ITag } from '../db/tag-store'; -import EventStore from '../db/event-store'; import NameExistsError from '../error/name-exists-error'; import { TAG_CREATED, TAG_DELETED } from '../types/events'; import { Logger } from '../logger'; +import { IUnleashStores } from '../types/stores'; +import { IUnleashConfig } from '../types/option'; +import { ITagStore } from '../types/stores/tag-store'; +import { IEventStore } from '../types/stores/event-store'; +import { ITag } from '../types/model'; export default class TagService { - private tagStore: TagStore; + private tagStore: ITagStore; - private eventStore: EventStore; + private eventStore: IEventStore; private logger: Logger; - constructor({ tagStore, eventStore }, { getLogger }) { + constructor( + { + tagStore, + eventStore, + }: Pick, + { getLogger }: Pick, + ) { this.tagStore = tagStore; this.eventStore = eventStore; this.logger = getLogger('services/tag-service.js'); @@ -54,7 +63,7 @@ export default class TagService { } async deleteTag(tag: ITag, userName: string): Promise { - await this.tagStore.deleteTag(tag); + await this.tagStore.delete(tag); await this.eventStore.store({ type: TAG_DELETED, createdBy: userName, diff --git a/src/lib/services/tag-type-schema.test.js b/src/lib/services/tag-type-schema.test.ts similarity index 93% rename from src/lib/services/tag-type-schema.test.js rename to src/lib/services/tag-type-schema.test.ts index d8f7399489..5147c6c2b7 100644 --- a/src/lib/services/tag-type-schema.test.js +++ b/src/lib/services/tag-type-schema.test.ts @@ -1,6 +1,4 @@ -'use strict'; - -const { tagTypeSchema } = require('./tag-type-schema'); +import { tagTypeSchema } from './tag-type-schema'; test('should require a URLFriendly name but allow empty description and icon', () => { const simpleTagType = { diff --git a/src/lib/services/tag-type-schema.ts b/src/lib/services/tag-type-schema.ts index 3580ac11cd..ec212fcf80 100644 --- a/src/lib/services/tag-type-schema.ts +++ b/src/lib/services/tag-type-schema.ts @@ -3,11 +3,7 @@ import { customJoi } from '../routes/admin-api/util'; export const tagTypeSchema = Joi.object() .keys({ - name: customJoi - .isUrlFriendly() - .min(2) - .max(50) - .required(), + name: customJoi.isUrlFriendly().min(2).max(50).required(), description: Joi.string().allow(''), icon: Joi.string().allow(''), }) diff --git a/src/lib/services/tag-type-service.ts b/src/lib/services/tag-type-service.ts index 04022f2f11..9ece507ba1 100644 --- a/src/lib/services/tag-type-service.ts +++ b/src/lib/services/tag-type-service.ts @@ -2,23 +2,32 @@ import NameExistsError from '../error/name-exists-error'; import { tagTypeSchema } from './tag-type-schema'; +import { IUnleashStores } from '../types/stores'; import { TAG_TYPE_CREATED, TAG_TYPE_DELETED, TAG_TYPE_UPDATED, } from '../types/events'; -import EventStore from '../db/event-store'; + import { Logger } from '../logger'; -import TagTypeStore, { ITagType } from '../db/tag-type-store'; +import { ITagType, ITagTypeStore } from '../types/stores/tag-type-store'; +import { IEventStore } from '../types/stores/event-store'; +import { IUnleashConfig } from '../types/option'; export default class TagTypeService { - private tagTypeStore: TagTypeStore; + private tagTypeStore: ITagTypeStore; - private eventStore: EventStore; + private eventStore: IEventStore; private logger: Logger; - constructor({ tagTypeStore, eventStore }, { getLogger }) { + constructor( + { + tagTypeStore, + eventStore, + }: Pick, + { getLogger }: Pick, + ) { this.tagTypeStore = tagTypeStore; this.eventStore = eventStore; this.logger = getLogger('services/tag-type-service.js'); @@ -29,7 +38,7 @@ export default class TagTypeService { } async getTagType(name: string): Promise { - return this.tagTypeStore.getTagType(name); + return this.tagTypeStore.get(name); } async createTagType( @@ -65,7 +74,7 @@ export default class TagTypeService { } async deleteTagType(name: string, userName: string): Promise { - await this.tagTypeStore.deleteTagType(name); + await this.tagTypeStore.delete(name); await this.eventStore.store({ type: TAG_TYPE_DELETED, createdBy: userName || 'unleash-system', diff --git a/src/lib/services/user-feedback-service.ts b/src/lib/services/user-feedback-service.ts index a25c0c3af5..89b1f4b3cd 100644 --- a/src/lib/services/user-feedback-service.ts +++ b/src/lib/services/user-feedback-service.ts @@ -1,11 +1,14 @@ import { Logger } from '../logger'; -import UserFeedbackStore, { IUserFeedback } from '../db/user-feedback-store'; import { IUnleashStores } from '../types/stores'; import { IUnleashConfig } from '../types/option'; import User from '../types/user'; +import { + IUserFeedback, + IUserFeedbackStore, +} from '../types/stores/user-feedback-store'; export default class UserFeedbackService { - private userFeedbackStore: UserFeedbackStore; + private userFeedbackStore: IUserFeedbackStore; private logger: Logger; diff --git a/src/lib/services/user-service.test.ts b/src/lib/services/user-service.test.ts index 83800ae14c..84485a35f2 100644 --- a/src/lib/services/user-service.test.ts +++ b/src/lib/services/user-service.test.ts @@ -2,7 +2,6 @@ import UserService from './user-service'; import UserStoreMock from '../../test/fixtures/fake-user-store'; import EventStoreMock from '../../test/fixtures/fake-event-store'; import AccessServiceMock from '../../test/fixtures/access-service-mock'; -import { ResetTokenStoreMock } from '../../test/fixtures/fake-reset-token-store'; import ResetTokenService from './reset-token-service'; import { EmailService } from './email-service'; import OwaspValidationError from '../error/owasp-validation-error'; @@ -11,6 +10,7 @@ import { createTestConfig } from '../../test/config/test-config'; import SessionService from './session-service'; import FakeSessionStore from '../../test/fixtures/fake-session-store'; import User from '../types/user'; +import FakeResetTokenStore from '../../test/fixtures/fake-reset-token-store'; const config: IUnleashConfig = createTestConfig(); @@ -20,9 +20,9 @@ test('Should create new user', async () => { const userStore = new UserStoreMock(); const eventStore = new EventStoreMock(); const accessService = new AccessServiceMock(); - const resetTokenStore = new ResetTokenStoreMock(); + const resetTokenStore = new FakeResetTokenStore(); const resetTokenService = new ResetTokenService( - { userStore, resetTokenStore }, + { resetTokenStore }, config, ); const sessionStore = new FakeSessionStore(); @@ -42,7 +42,7 @@ test('Should create new user', async () => { }, systemUser, ); - const storedUser = await userStore.get(user); + const storedUser = await userStore.get(user.id); const allUsers = await userStore.getAll(); expect(user.id).toBeTruthy(); @@ -55,9 +55,9 @@ test('Should create default user', async () => { const userStore = new UserStoreMock(); const eventStore = new EventStoreMock(); const accessService = new AccessServiceMock(); - const resetTokenStore = new ResetTokenStoreMock(); + const resetTokenStore = new FakeResetTokenStore(); const resetTokenService = new ResetTokenService( - { userStore, resetTokenStore }, + { resetTokenStore }, config, ); const emailService = new EmailService(config.email, config.getLogger); @@ -81,9 +81,9 @@ test('Should be a valid password', async () => { const userStore = new UserStoreMock(); const eventStore = new EventStoreMock(); const accessService = new AccessServiceMock(); - const resetTokenStore = new ResetTokenStoreMock(); + const resetTokenStore = new FakeResetTokenStore(); const resetTokenService = new ResetTokenService( - { userStore, resetTokenStore }, + { resetTokenStore }, config, ); @@ -107,9 +107,9 @@ test('Password must be at least 10 chars', async () => { const userStore = new UserStoreMock(); const eventStore = new EventStoreMock(); const accessService = new AccessServiceMock(); - const resetTokenStore = new ResetTokenStoreMock(); + const resetTokenStore = new FakeResetTokenStore(); const resetTokenService = new ResetTokenService( - { userStore, resetTokenStore }, + { resetTokenStore }, config, ); const emailService = new EmailService(config.email, config.getLogger); @@ -134,9 +134,9 @@ test('The password must contain at least one uppercase letter.', async () => { const userStore = new UserStoreMock(); const eventStore = new EventStoreMock(); const accessService = new AccessServiceMock(); - const resetTokenStore = new ResetTokenStoreMock(); + const resetTokenStore = new FakeResetTokenStore(); const resetTokenService = new ResetTokenService( - { userStore, resetTokenStore }, + { resetTokenStore }, config, ); const emailService = new EmailService(config.email, config.getLogger); @@ -162,9 +162,9 @@ test('The password must contain at least one number', async () => { const userStore = new UserStoreMock(); const eventStore = new EventStoreMock(); const accessService = new AccessServiceMock(); - const resetTokenStore = new ResetTokenStoreMock(); + const resetTokenStore = new FakeResetTokenStore(); const resetTokenService = new ResetTokenService( - { userStore, resetTokenStore }, + { resetTokenStore }, config, ); @@ -191,9 +191,9 @@ test('The password must contain at least one special character', async () => { const userStore = new UserStoreMock(); const eventStore = new EventStoreMock(); const accessService = new AccessServiceMock(); - const resetTokenStore = new ResetTokenStoreMock(); + const resetTokenStore = new FakeResetTokenStore(); const resetTokenService = new ResetTokenService( - { userStore, resetTokenStore }, + { resetTokenStore }, config, ); const emailService = new EmailService(config.email, config.getLogger); @@ -219,9 +219,9 @@ test('Should be a valid password with special chars', async () => { const userStore = new UserStoreMock(); const eventStore = new EventStoreMock(); const accessService = new AccessServiceMock(); - const resetTokenStore = new ResetTokenStoreMock(); + const resetTokenStore = new FakeResetTokenStore(); const resetTokenService = new ResetTokenService( - { userStore, resetTokenStore }, + { resetTokenStore }, config, ); const emailService = new EmailService(config.email, config.getLogger); diff --git a/src/lib/services/user-service.ts b/src/lib/services/user-service.ts index 38deec2065..5c8dcbd42a 100644 --- a/src/lib/services/user-service.ts +++ b/src/lib/services/user-service.ts @@ -4,11 +4,10 @@ import owasp from 'owasp-password-strength-test'; import Joi from 'joi'; import { URL } from 'url'; -import UserStore, { IUserSearch } from '../db/user-store'; import { Logger } from '../logger'; import User, { IUser } from '../types/user'; import isEmail from '../util/is-email'; -import { AccessService, RoleName } from './access-service'; +import { AccessService } from './access-service'; import ResetTokenService from './reset-token-service'; import InvalidTokenError from '../error/invalid-token-error'; import NotFoundError from '../error/notfound-error'; @@ -16,11 +15,12 @@ import OwaspValidationError from '../error/owasp-validation-error'; import { EmailService } from './email-service'; import { IUnleashConfig } from '../types/option'; import SessionService from './session-service'; -import { IUnleashServices } from '../types/services'; import { IUnleashStores } from '../types/stores'; import PasswordUndefinedError from '../error/password-undefined'; -import EventStore from '../db/event-store'; import { USER_UPDATED, USER_CREATED, USER_DELETED } from '../types/events'; +import { IEventStore } from '../types/stores/event-store'; +import { IUserSearch, IUserStore } from '../types/stores/user-store'; +import { RoleName } from '../types/model'; const systemUser = new User({ id: -1, username: 'system' }); @@ -58,9 +58,9 @@ const saltRounds = 10; class UserService { private logger: Logger; - private store: UserStore; + private store: IUserStore; - private eventStore: EventStore; + private eventStore: IEventStore; private accessService: AccessService; @@ -76,26 +76,20 @@ class UserService { getLogger, authentication, }: Pick, - { - accessService, - resetTokenService, - emailService, - sessionService, - }: Pick< - IUnleashServices, - | 'accessService' - | 'resetTokenService' - | 'emailService' - | 'sessionService' - >, + services: { + accessService: AccessService; + resetTokenService: ResetTokenService; + emailService: EmailService; + sessionService: SessionService; + }, ) { this.logger = getLogger('service/user-service.js'); this.store = stores.userStore; this.eventStore = stores.eventStore; - this.accessService = accessService; - this.resetTokenService = resetTokenService; - this.emailService = emailService; - this.sessionService = sessionService; + this.accessService = services.accessService; + this.resetTokenService = services.resetTokenService; + this.emailService = services.emailService; + this.sessionService = services.sessionService; if (authentication && authentication.createAdminUser) { process.nextTick(() => this.initAdminUser()); } @@ -128,7 +122,10 @@ class UserService { const passwordHash = await bcrypt.hash(pwd, saltRounds); await this.store.setPasswordHash(user.id, passwordHash); - await this.accessService.setUserRootRole(user.id, RoleName.ADMIN); + await this.accessService.setUserRootRole( + user.id, + RoleName.ADMIN, + ); } catch (e) { this.logger.error('Unable to create default user "admin"'); } @@ -141,8 +138,8 @@ class UserService { RoleName.VIEWER, ); const userRoles = await this.accessService.getRootRoleForAllUsers(); - const usersWithRootRole = users.map(u => { - const rootRole = userRoles.find(r => r.userId === u.id); + const usersWithRootRole = users.map((u) => { + const rootRole = userRoles.find((r) => r.userId === u.id); const roleId = rootRole ? rootRole.roleId : defaultRole.id; return { ...u, rootRole: roleId }; }); @@ -155,22 +152,22 @@ class UserService { RoleName.VIEWER, ); const roleId = roles.length > 0 ? roles[0].id : defaultRole.id; - const user = await this.store.get({ id }); + const user = await this.store.get(id); return { ...user, rootRole: roleId }; } - async search(query: IUserSearch): Promise { + async search(query: IUserSearch): Promise { return this.store.search(query); } - async getByEmail(email: string): Promise { - return this.store.get({ email }); + async getByEmail(email: string): Promise { + return this.store.getByQuery({ email }); } async createUser( { username, email, name, password, rootRole }: ICreateUser, updatedBy?: User, - ): Promise { + ): Promise { assert.ok(username || email, 'You must specify username or email'); if (email) { @@ -202,7 +199,7 @@ class UserService { private async updateChangeLog( type: string, - user: User, + user: IUser, updatedBy: User = systemUser, ): Promise { await this.eventStore.store({ @@ -220,7 +217,7 @@ class UserService { async updateUser( { id, name, email, rootRole }: IUpdateUser, updatedBy?: User, - ): Promise { + ): Promise { if (email) { Joi.assert(email, Joi.string().email(), 'Email'); } @@ -236,11 +233,11 @@ class UserService { return user; } - async loginUser(usernameOrEmail: string, password: string): Promise { + async loginUser(usernameOrEmail: string, password: string): Promise { const idQuery = isEmail(usernameOrEmail) ? { email: usernameOrEmail } : { username: usernameOrEmail }; - const user = await this.store.get(idQuery); + const user = await this.store.getByQuery(idQuery); const passwordHash = await this.store.getPasswordHash(user.id); const match = await bcrypt.compare(password, passwordHash); @@ -262,11 +259,11 @@ class UserService { async loginUserWithoutPassword( email: string, autoCreateUser: boolean = false, - ): Promise { - let user: User; + ): Promise { + let user: IUser; try { - user = await this.store.get({ email }); + user = await this.store.getByQuery({ email }); } catch (e) { if (autoCreateUser) { const defaultRole = await this.accessService.getRootRole( @@ -291,10 +288,10 @@ class UserService { } async deleteUser(userId: number, updatedBy?: User): Promise { - const user = await this.store.get({ id: userId }); + const user = await this.store.get(userId); const roles = await this.accessService.getRolesForUser(userId); await Promise.all( - roles.map(role => + roles.map((role) => this.accessService.removeUserFromRole(userId, role.id), ), ); diff --git a/src/lib/services/version-service.test.js b/src/lib/services/version-service.test.ts similarity index 66% rename from src/lib/services/version-service.test.js rename to src/lib/services/version-service.test.ts index 2e07a2030f..117a5e5a32 100644 --- a/src/lib/services/version-service.test.js +++ b/src/lib/services/version-service.test.ts @@ -1,113 +1,113 @@ -const fetchMock = require('fetch-mock').sandbox(); -const stores = require('../../test/fixtures/store'); -const getLogger = require('../../test/fixtures/no-logger'); -const version = require('../util/version'); +import fetchMock from 'jest-fetch-mock'; +import createStores from '../../test/fixtures/store'; +import version from '../util/version'; +import getLogger from '../../test/fixtures/no-logger'; -jest.mock('node-fetch', () => fetchMock); +import VersionService from './version-service'; -const VersionService = require('./version-service'); +beforeEach(() => { + fetchMock.resetMocks(); +}); test('yields current versions', async () => { const testurl = 'https://version.test'; - const { settingStore } = stores.createStores(); - await settingStore.insert({ - name: 'instanceInfo', - content: { id: '1234abc' }, - }); + const { settingStore } = createStores(); + await settingStore.insert('instanceInfo', { id: '1234abc' }); const latest = { oss: '5.0.0', enterprise: '5.0.0', }; - fetchMock.mock( - { url: testurl, method: 'POST' }, - { + fetchMock.mockResponse( + JSON.stringify({ latest: false, versions: latest, - }, - ); - const service = new VersionService( - { settingStore }, - { getLogger, versionCheck: { url: testurl, enable: true } }, - ); - await service.checkLatestVersion(); - fetchMock.done(); - const versionInfo = service.getVersionInfo(); - expect(versionInfo.current.oss).toBe(version); - expect(versionInfo.current.enterprise).toBeFalsy(); - expect(versionInfo.latest.oss).toBe(latest.oss); - expect(versionInfo.latest.enterprise).toBe(latest.enterprise); -}); - -test('supports setting enterprise version as well', async () => { - const testurl = `https://version.test${Math.random() * 1000}`; - const { settingStore } = stores.createStores(); - const enterpriseVersion = '3.7.0'; - await settingStore.insert({ - name: 'instanceInfo', - content: { id: '1234abc' }, - }); - const latest = { - oss: '4.0.0', - enterprise: '4.0.0', - }; - fetchMock.mock( - { url: testurl, method: 'POST' }, - { - latest: false, - versions: latest, - }, + }), + { status: 200 }, ); const service = new VersionService( { settingStore }, { getLogger, versionCheck: { url: testurl, enable: true }, - version, + }, + ); + await service.checkLatestVersion(); + const versionInfo = service.getVersionInfo(); + expect(versionInfo.current.oss).toBe(version); + expect(versionInfo.current.enterprise).toBeFalsy(); + // @ts-ignore + expect(versionInfo.latest.oss).toBe(latest.oss); + // @ts-ignore + expect(versionInfo.latest.enterprise).toBe(latest.enterprise); +}); + +test('supports setting enterprise version as well', async () => { + const testurl = `https://version.test${Math.random() * 1000}`; + const { settingStore } = createStores(); + const enterpriseVersion = '3.7.0'; + await settingStore.insert('instanceInfo', { id: '1234abc' }); + const latest = { + oss: '4.0.0', + enterprise: '4.0.0', + }; + fetchMock.mockResponse( + JSON.stringify({ + latest: false, + versions: latest, + }), + { status: 200 }, + ); + + const service = new VersionService( + { settingStore }, + { + getLogger, + versionCheck: { url: testurl, enable: true }, enterpriseVersion, }, ); await service.checkLatestVersion(); - fetchMock.done(); const versionInfo = service.getVersionInfo(); expect(versionInfo.current.oss).toBe(version); expect(versionInfo.current.enterprise).toBe(enterpriseVersion); + // @ts-ignore expect(versionInfo.latest.oss).toBe(latest.oss); + // @ts-ignore expect(versionInfo.latest.enterprise).toBe(latest.enterprise); }); test('if version check is not enabled should not make any calls', async () => { const testurl = `https://version.test${Math.random() * 1000}`; - const { settingStore } = stores.createStores(); + const { settingStore } = createStores(); const enterpriseVersion = '3.7.0'; - await settingStore.insert({ - name: 'instanceInfo', - content: { id: '1234abc' }, - }); + await settingStore.insert('instanceInfo', { id: '1234abc' }); const latest = { oss: '4.0.0', enterprise: '4.0.0', }; - fetchMock.mock( - { url: testurl, method: 'POST' }, - { + fetchMock.mockResponse( + JSON.stringify({ latest: false, versions: latest, - }, + }), + { status: 200 }, ); + const service = new VersionService( { settingStore }, { getLogger, versionCheck: { url: testurl, enable: false }, - version, enterpriseVersion, }, ); await service.checkLatestVersion(); - expect(fetchMock.called(testurl)).toBe(false); const versionInfo = service.getVersionInfo(); + expect(fetchMock.mock.calls).toHaveLength(0); expect(versionInfo.current.oss).toBe(version); expect(versionInfo.current.enterprise).toBe(enterpriseVersion); + // @ts-ignore expect(versionInfo.latest.oss).toBeFalsy(); + // @ts-ignore expect(versionInfo.latest.enterprise).toBeFalsy(); }); diff --git a/src/lib/services/version-service.ts b/src/lib/services/version-service.ts index 937d36e220..5407f4f2cd 100644 --- a/src/lib/services/version-service.ts +++ b/src/lib/services/version-service.ts @@ -3,7 +3,7 @@ import { IUnleashStores } from '../types/stores'; import { IUnleashConfig } from '../types/option'; import version from '../util/version'; import { Logger } from '../logger'; -import SettingStore from '../db/setting-store'; +import { ISettingStore } from '../types/stores/settings-store'; const TWO_DAYS = 48 * 60 * 60 * 1000; @@ -22,7 +22,7 @@ export interface IVersionHolder { export default class VersionService { private logger: Logger; - private settingStore: SettingStore; + private settingStore: ISettingStore; private current: IVersionInfo; @@ -45,8 +45,8 @@ export default class VersionService { versionCheck, enterpriseVersion, }: Pick< - IUnleashConfig, - 'getLogger' | 'versionCheck' | 'enterpriseVersion' + IUnleashConfig, + 'getLogger' | 'versionCheck' | 'enterpriseVersion' >, ) { this.logger = getLogger('lib/services/version-service.js'); diff --git a/src/lib/types/index.ts b/src/lib/types/index.ts new file mode 100644 index 0000000000..6ba2953b55 --- /dev/null +++ b/src/lib/types/index.ts @@ -0,0 +1,2 @@ +export * from './services'; +export * from './stores'; diff --git a/src/lib/types/model.ts b/src/lib/types/model.ts index a3b8cf5d33..e9a58b1586 100644 --- a/src/lib/types/model.ts +++ b/src/lib/types/model.ts @@ -1,4 +1,7 @@ -import { IFeatureStrategy } from '../db/feature-strategy-store'; +import { ITagType } from './stores/tag-type-store'; +import { LogProvider } from '../logger'; +import { IRole, IUserPermission } from './stores/access-store'; +import { IUser } from './user'; export interface IConstraint { contextName: string; @@ -12,6 +15,16 @@ export interface IStrategyConfig { constraints: IConstraint[]; parameters: Object; } +export interface IFeatureStrategy { + id: string; + featureName: string; + projectName: string; + environment: string; + strategyName: string; + parameters: object; + constraints: IConstraint[]; + createdAt?: Date; +} export interface FeatureToggleDTO { name: string; @@ -132,3 +145,143 @@ export interface ITag { value: string; type: string; } + +export interface IParameterDefinition { + name: string; + displayName: string; + type: string; + description?: string; + placeholder?: string; + required: boolean; + sensitive: boolean; +} + +export interface IAddonDefinition { + name: string; + displayName: string; + documentationUrl: string; + description: string; + parameters?: IParameterDefinition[]; + events?: string[]; + tagTypes?: ITagType[]; +} + +export interface IAddonConfig { + getLogger: LogProvider; + unleashUrl: string; +} + +export interface ICreateEvent { + type: string; + createdBy: string; + data?: any; + tags?: ITag[]; +} + +export interface IEvent extends ICreateEvent { + id: number; + createdAt: Date; +} + +export interface IUserWithRole { + id: number; + roleId: number; + name?: string; + username?: string; + email?: string; + imageUrl?: string; +} + +export interface IRoleData { + role: IRole; + users: IUser[]; + permissions: IUserPermission[]; +} + +export interface IPermission { + name: string; + type: PermissionType; +} + +export enum PermissionType { + root = 'root', + project = 'project', +} + +export enum RoleName { + // eslint-disable-next-line @typescript-eslint/no-shadow + ADMIN = 'Admin', + EDITOR = 'Editor', + VIEWER = 'Viewer', + OWNER = 'Owner', + MEMBER = 'Member', +} + +export enum RoleType { + ROOT = 'root', + PROJECT = 'project', +} + +export interface IRoleIdentifier { + roleId?: number; + roleName?: RoleName; +} + +export interface IClientApp { + appName: string; + instanceId: string; + clientIp?: string; + seenToggles?: string[]; + metricsCount?: number; + strategies?: string[] | Record[]; + bucket?: any; + count?: number; + started?: number | Date; + interval?: number; + icon?: string; + description?: string; + color?: string; +} + +export interface IAppFeature { + name: string; + description: string; + type: string; + project: string; + enabled: boolean; + stale: boolean; + strategies: any; + variants: any[]; + createdAt: Date; + lastSeenAt: Date; +} + +export interface IAppName { + appName: string; +} + +export interface IMetricCounts { + yes?: number; + no?: number; + variants?: Record; +} + +export interface IMetricsBucket { + start: Date; + stop: Date; + toggles: IMetricCounts; +} + +export interface IImportFile extends ImportCommon { + file: string; +} + +interface ImportCommon { + dropBeforeImport?: boolean; + keepExisting?: boolean; + userName?: string; +} + +export interface IImportData extends ImportCommon { + data: any; +} diff --git a/src/lib/types/query.ts b/src/lib/types/query.ts new file mode 100644 index 0000000000..a9dfb99057 --- /dev/null +++ b/src/lib/types/query.ts @@ -0,0 +1,3 @@ +export interface IApplicationQuery { + strategyName?: string; +} diff --git a/src/lib/types/services.ts b/src/lib/types/services.ts index 8916605dc9..2c7bb6849f 100644 --- a/src/lib/types/services.ts +++ b/src/lib/types/services.ts @@ -32,9 +32,11 @@ export interface IUnleashServices { emailService: EmailService; environmentService: EnvironmentService; eventService: EventService; + featureTagService: FeatureTagService; featureToggleServiceV2: FeatureToggleServiceV2; featureTypeService: FeatureTypeService; healthService: HealthService; + projectHealthService: ProjectHealthService; projectService: ProjectService; resetTokenService: ResetTokenService; sessionService: SessionService; @@ -46,6 +48,4 @@ export interface IUnleashServices { userFeedbackService: UserFeedbackService; userService: UserService; versionService: VersionService; - featureTagService: FeatureTagService; - projectHealthService: ProjectHealthService; } diff --git a/src/lib/types/stores.ts b/src/lib/types/stores.ts index 937a39b438..1144eb0e8e 100644 --- a/src/lib/types/stores.ts +++ b/src/lib/types/stores.ts @@ -1,49 +1,49 @@ -import { Knex } from 'knex'; -import ProjectStore from '../db/project-store'; -import EventStore from '../db/event-store'; -import FeatureTypeStore from '../db/feature-type-store'; -import StrategyStore from '../db/strategy-store'; -import ClientApplicationsDb from '../db/client-applications-store'; -import ClientInstanceStore from '../db/client-instance-store'; -import { ClientMetricsStore } from '../db/client-metrics-store'; -import FeatureToggleStore from '../db/feature-toggle-store'; -import ContextFieldStore from '../db/context-field-store'; -import SettingStore from '../db/setting-store'; -import UserStore from '../db/user-store'; -import TagStore from '../db/tag-store'; -import TagTypeStore from '../db/tag-type-store'; -import AddonStore from '../db/addon-store'; -import UserFeedbackStore from '../db/user-feedback-store'; -import { AccessStore } from '../db/access-store'; -import { ApiTokenStore } from '../db/api-token-store'; -import { ResetTokenStore } from '../db/reset-token-store'; -import SessionStore from '../db/session-store'; -import FeatureStrategiesStore from '../db/feature-strategy-store'; -import EnvironmentStore from '../db/environment-store'; -import FeatureTagStore from '../db/feature-tag-store'; +import { IProjectStore } from './stores/project-store'; +import { IEventStore } from './stores/event-store'; +import { IFeatureTypeStore } from './stores/feature-type-store'; +import { IStrategyStore } from './stores/strategy-store'; +import { IClientApplicationsStore } from './stores/client-applications-store'; +import { IClientInstanceStore } from './stores/client-instance-store'; +import { IClientMetricsStore } from './stores/client-metrics-store'; +import { IFeatureToggleStore } from './stores/feature-toggle-store'; +import { IContextFieldStore } from './stores/context-field-store'; +import { ISettingStore } from './stores/settings-store'; +import { ISessionStore } from './stores/session-store'; +import { ITagStore } from './stores/tag-store'; +import { ITagTypeStore } from './stores/tag-type-store'; +import { IFeatureTagStore } from './stores/feature-tag-store'; +import { IUserStore } from './stores/user-store'; +import { IAddonStore } from './stores/addon-store'; +import { IAccessStore } from './stores/access-store'; +import { IApiTokenStore } from './stores/api-token-store'; +import { IResetTokenStore } from './stores/reset-token-store'; +import { IUserFeedbackStore } from './stores/user-feedback-store'; +import { IFeatureEnvironmentStore } from './stores/feature-environment-store'; +import { IFeatureStrategiesStore } from './stores/feature-strategies-store'; +import { IEnvironmentStore } from './stores/environment-store'; export interface IUnleashStores { - projectStore: ProjectStore; - eventStore: EventStore; - featureTypeStore: FeatureTypeStore; - strategyStore: StrategyStore; - clientApplicationsStore: ClientApplicationsDb; - clientInstanceStore: ClientInstanceStore; - clientMetricsStore: ClientMetricsStore; - featureToggleStore: FeatureToggleStore; - contextFieldStore: ContextFieldStore; - settingStore: SettingStore; - sessionStore: SessionStore; - userStore: UserStore; - tagStore: TagStore; - tagTypeStore: TagTypeStore; - addonStore: AddonStore; - accessStore: AccessStore; - apiTokenStore: ApiTokenStore; - resetTokenStore: ResetTokenStore; - userFeedbackStore: UserFeedbackStore; - featureStrategiesStore: FeatureStrategiesStore; - environmentStore: EnvironmentStore; - featureTagStore: FeatureTagStore; - db: Knex; + accessStore: IAccessStore; + addonStore: IAddonStore; + apiTokenStore: IApiTokenStore; + clientApplicationsStore: IClientApplicationsStore; + clientInstanceStore: IClientInstanceStore; + clientMetricsStore: IClientMetricsStore; + contextFieldStore: IContextFieldStore; + environmentStore: IEnvironmentStore; + eventStore: IEventStore; + featureEnvironmentStore: IFeatureEnvironmentStore; + featureStrategiesStore: IFeatureStrategiesStore; + featureTagStore: IFeatureTagStore; + featureToggleStore: IFeatureToggleStore; + featureTypeStore: IFeatureTypeStore; + projectStore: IProjectStore; + resetTokenStore: IResetTokenStore; + sessionStore: ISessionStore; + settingStore: ISettingStore; + strategyStore: IStrategyStore; + tagStore: ITagStore; + tagTypeStore: ITagTypeStore; + userFeedbackStore: IUserFeedbackStore; + userStore: IUserStore; } diff --git a/src/lib/types/stores/access-store.ts b/src/lib/types/stores/access-store.ts new file mode 100644 index 0000000000..9adf5c1a2a --- /dev/null +++ b/src/lib/types/stores/access-store.ts @@ -0,0 +1,49 @@ +import { Store } from './store'; + +export interface IUserPermission { + project?: string; + permission: string; +} + +export interface IRole { + id: number; + name: string; + description?: string; + type: string; + project?: string; +} + +export interface IUserRole { + roleId: number; + userId: number; +} +export interface IAccessStore extends Store { + getPermissionsForUser(userId: number): Promise; + getPermissionsForRole(roleId: number): Promise; + getRoles(): Promise; + getRolesForProject(projectId: string): Promise; + getRootRoles(): Promise; + removeRolesForProject(projectId: string): Promise; + getRolesForUserId(userId: number): Promise; + getUserIdsForRole(roleId: number): Promise; + addUserToRole(userId: number, roleId: number): Promise; + removeUserFromRole(userId: number, roleId: number): Promise; + removeRolesOfTypeForUser(userId: number, roleType: string): Promise; + createRole( + name: string, + type: string, + project?: string, + description?: string, + ): Promise; + addPermissionsToRole( + role_id: number, + permissions: string[], + projectId?: string, + ): Promise; + removePermissionFromRole( + roleId: number, + permission: string, + projectId?: string, + ): Promise; + getRootRoleForAllUsers(): Promise; +} diff --git a/src/lib/types/stores/addon-store.ts b/src/lib/types/stores/addon-store.ts new file mode 100644 index 0000000000..7dadb54fb5 --- /dev/null +++ b/src/lib/types/stores/addon-store.ts @@ -0,0 +1,19 @@ +import { Store } from './store'; + +export interface IAddonDto { + provider: string; + description: string; + enabled: boolean; + parameters: object; + events: string[]; +} + +export interface IAddon extends IAddonDto { + id: number; + createdAt: Date; +} + +export interface IAddonStore extends Store { + insert(addon: IAddonDto): Promise; + update(id: number, addon: IAddonDto): Promise; +} diff --git a/src/lib/types/stores/api-token-store.ts b/src/lib/types/stores/api-token-store.ts new file mode 100644 index 0000000000..9d89c36c88 --- /dev/null +++ b/src/lib/types/stores/api-token-store.ts @@ -0,0 +1,25 @@ +import { Store } from './store'; + +export enum ApiTokenType { + CLIENT = 'client', + ADMIN = 'admin', +} + +export interface IApiTokenCreate { + secret: string; + username: string; + type: ApiTokenType; + expiresAt?: Date; +} + +export interface IApiToken extends IApiTokenCreate { + createdAt: Date; + seenAt?: Date; +} + +export interface IApiTokenStore extends Store { + getAllActive(): Promise; + insert(newToken: IApiTokenCreate): Promise; + setExpiry(secret: string, expiresAt: Date): Promise; + markSeenAt(secrets: string[]): Promise; +} diff --git a/src/lib/types/stores/client-applications-store.ts b/src/lib/types/stores/client-applications-store.ts new file mode 100644 index 0000000000..8548143ffc --- /dev/null +++ b/src/lib/types/stores/client-applications-store.ts @@ -0,0 +1,25 @@ +import { Store } from './store'; +import { IApplicationQuery } from '../query'; + +export interface IClientApplication { + appName: string; + updatedAt: Date; + createdAt: Date; + lastSeen: Date; + description: string; + createdBy: string; + announced: boolean; + url: string; + color: string; + icon: string; + strategies: string[]; +} + +export interface IClientApplicationsStore + extends Store { + upsert(details: Partial): Promise; + bulkUpsert(details: Partial[]): Promise; + getAppsForStrategy(query: IApplicationQuery): Promise; + getUnannounced(): Promise; + setUnannouncedToAnnounced(): Promise; +} diff --git a/src/lib/types/stores/client-instance-store.ts b/src/lib/types/stores/client-instance-store.ts new file mode 100644 index 0000000000..ffa2fcf054 --- /dev/null +++ b/src/lib/types/stores/client-instance-store.ts @@ -0,0 +1,24 @@ +import { Store } from './store'; + +export interface IClientInstance extends INewClientInstance { + createdAt: Date; +} + +export interface INewClientInstance { + appName: string; + instanceId: string; + sdkVersion?: string; + clientIp?: string; + lastSeen?: Date; +} +export interface IClientInstanceStore + extends Store< + IClientInstance, + Pick + > { + bulkUpsert(instances: INewClientInstance[]): Promise; + insert(details: INewClientInstance): Promise; + getByAppName(appName: string): Promise; + getDistinctApplications(): Promise; + deleteForApplication(appName: string): Promise; +} diff --git a/src/lib/types/stores/client-metrics-db.ts b/src/lib/types/stores/client-metrics-db.ts new file mode 100644 index 0000000000..0d1b46102c --- /dev/null +++ b/src/lib/types/stores/client-metrics-db.ts @@ -0,0 +1,12 @@ +export interface IClientMetric { + id: number; + createdAt: Date; + metrics: any; +} + +export interface IClientMetricsDb { + removeMetricsOlderThanOneHour(): Promise; + insert(metrics: IClientMetric); + getMetricsLastHour(): Promise; + getNewMetrics(lastKnownId: number): Promise; +} diff --git a/src/lib/types/stores/client-metrics-store.ts b/src/lib/types/stores/client-metrics-store.ts new file mode 100644 index 0000000000..ccea28f751 --- /dev/null +++ b/src/lib/types/stores/client-metrics-store.ts @@ -0,0 +1,9 @@ +import EventEmitter from 'events'; +import { IClientMetric } from './client-metrics-db'; +import { Store } from './store'; + +export interface IClientMetricsStore + extends Store, + EventEmitter { + insert(metrics: IClientMetric): Promise; +} diff --git a/src/lib/types/stores/context-field-store.ts b/src/lib/types/stores/context-field-store.ts new file mode 100644 index 0000000000..48ef5c29f3 --- /dev/null +++ b/src/lib/types/stores/context-field-store.ts @@ -0,0 +1,18 @@ +import { Store } from './store'; + +export interface IContextFieldDto { + name: string; + description: string; + stickiness: boolean; + sortOrder: number; + legalValues?: string[]; +} + +export interface IContextField extends IContextFieldDto { + createdAt: Date; +} + +export interface IContextFieldStore extends Store { + create(data: IContextFieldDto): Promise; + update(data: IContextFieldDto): Promise; +} diff --git a/src/lib/types/stores/environment-store.ts b/src/lib/types/stores/environment-store.ts new file mode 100644 index 0000000000..1c44cba221 --- /dev/null +++ b/src/lib/types/stores/environment-store.ts @@ -0,0 +1,17 @@ +import { IEnvironment } from '../model'; +import { Store } from './store'; + +export interface IEnvironmentStore extends Store { + exists(name: string): Promise; + upsert(env: IEnvironment): Promise; + connectProject(environment: string, projectId: string): Promise; + connectFeatures(environment: string, projectId: string): Promise; + disconnectProjectFromEnv( + environment: string, + projectId: string, + ): Promise; + connectFeatureToEnvironmentsForProject( + featureName: string, + project_id: string, + ): Promise; +} diff --git a/src/lib/types/stores/event-store.ts b/src/lib/types/stores/event-store.ts new file mode 100644 index 0000000000..3f3965ac31 --- /dev/null +++ b/src/lib/types/stores/event-store.ts @@ -0,0 +1,10 @@ +import EventEmitter from 'events'; +import { ICreateEvent, IEvent } from '../model'; +import { Store } from './store'; + +export interface IEventStore extends Store, EventEmitter { + store(event: ICreateEvent): Promise; + batchStore(events: ICreateEvent[]): Promise; + getEvents(): Promise; + getEventsFilterByType(name: string): Promise; +} diff --git a/src/lib/types/stores/feature-environment-store.ts b/src/lib/types/stores/feature-environment-store.ts new file mode 100644 index 0000000000..f38942dd0e --- /dev/null +++ b/src/lib/types/stores/feature-environment-store.ts @@ -0,0 +1,46 @@ +import { IFeatureEnvironment } from '../model'; +import { Store } from './store'; + +export interface FeatureEnvironmentKey { + featureName: string; + environment: string; +} + +export interface IFeatureEnvironmentStore + extends Store { + getAllFeatureEnvironments(): Promise; + featureHasEnvironment( + environment: string, + featureName: string, + ): Promise; + isEnvironmentEnabled( + featureName: string, + environment: string, + ): Promise; + toggleEnvironmentEnabledStatus( + environment: string, + featureName: string, + enabled: boolean, + ): Promise; + getEnvironmentMetaData( + environment: string, + featureName: string, + ): Promise; + disconnectEnvironmentFromProject( + environment: string, + project: string, + ): Promise; + removeEnvironmentForFeature( + feature_name: string, + environment: string, + ): Promise; + connectEnvironmentAndFeature( + feature_name: string, + environment: string, + enabled: boolean, + ): Promise; + enableEnvironmentForFeature( + feature_name: string, + environment: string, + ): Promise; +} diff --git a/src/lib/types/stores/feature-strategies-store.ts b/src/lib/types/stores/feature-strategies-store.ts new file mode 100644 index 0000000000..bb0b73593b --- /dev/null +++ b/src/lib/types/stores/feature-strategies-store.ts @@ -0,0 +1,62 @@ +import { + FeatureToggleWithEnvironment, + IFeatureStrategy, + IFeatureToggleClient, + IFeatureToggleQuery, + IStrategyConfig, + IVariant, +} from '../model'; +import { Store } from './store'; + +export interface FeatureConfigurationClient { + name: string; + type: string; + enabled: boolean; + stale: boolean; + strategies: IStrategyConfig[]; + variants: IVariant[]; +} +export interface IFeatureStrategiesStore + extends Store { + createStrategyConfig( + strategyConfig: Omit, + ): Promise; + getStrategiesForToggle(featureName: string): Promise; + getAllFeatureStrategies(): Promise; + getStrategiesForEnvironment( + environment: string, + ): Promise; + removeAllStrategiesForEnv( + feature_name: string, + environment: string, + ): Promise; + getAll(): Promise; + getStrategiesForFeature( + project_name: string, + feature_name: string, + environment: string, + ): Promise; + getStrategiesForEnv(environment: string): Promise; + getFeatureToggleAdmin( + featureName: string, + archived: boolean, + ): Promise; + getFeatures( + featureQuery: Partial, + archived: boolean, + isAdmin: boolean, + ): Promise; + getStrategyById(id: string): Promise; + updateStrategy( + id: string, + updates: Partial, + ): Promise; + getStrategiesAndMetadataForEnvironment( + environment: string, + featureName: string, + ): Promise; + deleteConfigurationsForProjectAndEnvironment( + projectId: String, + environment: String, + ): Promise; +} diff --git a/src/lib/types/stores/feature-tag-store.ts b/src/lib/types/stores/feature-tag-store.ts new file mode 100644 index 0000000000..7f3fdb7cda --- /dev/null +++ b/src/lib/types/stores/feature-tag-store.ts @@ -0,0 +1,19 @@ +import { ITag } from '../model'; +import { Store } from './store'; + +export interface IFeatureTag { + featureName: string; + tagType: string; + tagValue: string; +} + +export interface IFeatureAndTag { + featureName: string; + tag: ITag; +} +export interface IFeatureTagStore extends Store { + getAllTagsForFeature(featureName: string): Promise; + tagFeature(featureName: string, tag: ITag): Promise; + importFeatureTags(featureTags: IFeatureTag[]): Promise; + untagFeature(featureName: string, tag: ITag): Promise; +} diff --git a/src/lib/types/stores/feature-toggle-store.ts b/src/lib/types/stores/feature-toggle-store.ts new file mode 100644 index 0000000000..89b74441ea --- /dev/null +++ b/src/lib/types/stores/feature-toggle-store.ts @@ -0,0 +1,35 @@ +import { FeatureToggle, FeatureToggleDTO } from '../model'; +import { Store } from './store'; + +export interface IFeatureToggleQuery { + archived: boolean; + project: string; + stale: boolean; +} + +export interface IHasFeature { + name: string; + archived: boolean; +} + +export interface IFeatureToggleStore extends Store { + count(query: Partial): Promise; + getFeatureMetadata(name: string): Promise; + getFeatures(archived: boolean): Promise; + hasFeature(name: string): Promise; + updateLastSeenForToggles(toggleNames: string[]): Promise; + getProjectId(name: string): Promise; + createFeature( + project: string, + data: FeatureToggleDTO, + ): Promise; + updateFeature( + project: string, + data: FeatureToggleDTO, + ): Promise; + archiveFeature(featureName: string): Promise; + reviveFeature(featureName: string): Promise; + getFeaturesBy( + query: Partial, + ): Promise; +} diff --git a/src/lib/types/stores/feature-type-store.ts b/src/lib/types/stores/feature-type-store.ts new file mode 100644 index 0000000000..4776eb1fff --- /dev/null +++ b/src/lib/types/stores/feature-type-store.ts @@ -0,0 +1,12 @@ +import { Store } from './store'; + +export interface IFeatureType { + id: number; + name: string; + description: string; + lifetimeDays: number; +} + +export interface IFeatureTypeStore extends Store { + getByName(name: string): Promise; +} diff --git a/src/lib/types/stores/project-store.ts b/src/lib/types/stores/project-store.ts new file mode 100644 index 0000000000..5dc205e9b3 --- /dev/null +++ b/src/lib/types/stores/project-store.ts @@ -0,0 +1,41 @@ +import { IFeatureOverview } from '../model'; +import { Store } from './store'; + +export interface IProject { + id: string; + name: string; + description: string; + health: number; + createdAt: Date; +} +export interface IProjectInsert { + id: string; + name: string; + description: string; +} + +export interface IProjectArchived { + id: string; + archived: boolean; +} + +export interface IProjectHealthUpdate { + id: string; + health: number; +} + +export interface IProjectStore extends Store { + hasProject(id: string): Promise; + updateHealth(healthUpdate: IProjectHealthUpdate): Promise; + create(project: IProjectInsert): Promise; + update(update: IProjectInsert): Promise; + importProjects(projects: IProjectInsert[]): Promise; + addEnvironmentToProject(id: string, environment: string): Promise; + deleteEnvironmentForProject(id: string, environment: string): Promise; + getEnvironmentsForProject(id: string): Promise; + getMembers(projectId: string): Promise; + getProjectOverview( + projectId: string, + archived: boolean, + ): Promise; +} diff --git a/src/lib/types/stores/reset-token-store.ts b/src/lib/types/stores/reset-token-store.ts new file mode 100644 index 0000000000..19cf4d6c63 --- /dev/null +++ b/src/lib/types/stores/reset-token-store.ts @@ -0,0 +1,37 @@ +import { Store } from './store'; + +export interface IResetTokenCreate { + reset_token: string; + user_id: number; + expires_at: Date; + created_by?: string; +} + +export interface IResetToken { + userId: number; + token: string; + createdBy: string; + expiresAt: Date; + createdAt: Date; + usedAt?: Date; +} + +export interface IResetQuery { + userId: number; + token: string; +} + +export interface IResetTokenQuery { + user_id: number; + reset_token: string; +} + +export interface IResetTokenStore extends Store { + getActive(token: string): Promise; + getActiveTokens(): Promise; + insert(newToken: IResetTokenCreate): Promise; + useToken(token: IResetQuery): Promise; + deleteFromQuery(query: IResetTokenQuery): Promise; + deleteExpired(): Promise; + expireExistingTokensForUser(user_id: number): Promise; +} diff --git a/src/lib/types/stores/session-store.ts b/src/lib/types/stores/session-store.ts new file mode 100644 index 0000000000..38cd159165 --- /dev/null +++ b/src/lib/types/stores/session-store.ts @@ -0,0 +1,15 @@ +import { Store } from './store'; + +export interface ISession { + sid: string; + sess: any; + createdAt: Date; + expired?: Date; +} + +export interface ISessionStore extends Store { + getActiveSessions(): Promise; + getSessionsForUser(userId: number): Promise; + deleteSessionsForUser(userId: number): Promise; + insertSession(data: Omit): Promise; +} diff --git a/src/lib/types/stores/settings-store.ts b/src/lib/types/stores/settings-store.ts new file mode 100644 index 0000000000..ef9b396dd7 --- /dev/null +++ b/src/lib/types/stores/settings-store.ts @@ -0,0 +1,11 @@ +import { Store } from './store'; + +export interface ISettingInsert { + name: string; + content: any; +} + +export interface ISettingStore extends Store { + insert(name: string, content: any): Promise; + updateRow(name: string, content: any): Promise; +} diff --git a/src/lib/types/stores/store.ts b/src/lib/types/stores/store.ts new file mode 100644 index 0000000000..fd1356a6ce --- /dev/null +++ b/src/lib/types/stores/store.ts @@ -0,0 +1,8 @@ +export interface Store { + get(key: K): Promise; + getAll(): Promise; + exists(key: K): Promise; + delete(key: K): Promise; + deleteAll(): Promise; + destroy(): void; +} diff --git a/src/lib/types/stores/strategy-store.ts b/src/lib/types/stores/strategy-store.ts new file mode 100644 index 0000000000..090812a8b2 --- /dev/null +++ b/src/lib/types/stores/strategy-store.ts @@ -0,0 +1,40 @@ +import { Store } from './store'; + +export interface IStrategy { + name: string; + editable: boolean; + description: string; + parameters: object; + deprecated: boolean; + displayName: string; +} + +export interface IEditableStrategy { + name: string; + description?: string; + parameters: object; + deprecated: boolean; +} + +export interface IMinimalStrategy { + name: string; + description?: string; + editable?: boolean; + parameters?: any[]; +} + +export interface IMinimalStrategyRow { + name: string; + description?: string; + editable?: boolean; + parameters?: string; +} + +export interface IStrategyStore extends Store { + getEditableStrategies(): Promise; + createStrategy(update: IMinimalStrategy): Promise; + updateStrategy(update: IMinimalStrategy): Promise; + deprecateStrategy({ name }: Pick): Promise; + reactivateStrategy({ name }: Pick): Promise; + importStrategy(data: IMinimalStrategy): Promise; +} diff --git a/src/lib/types/stores/tag-store.ts b/src/lib/types/stores/tag-store.ts new file mode 100644 index 0000000000..a5d2e388da --- /dev/null +++ b/src/lib/types/stores/tag-store.ts @@ -0,0 +1,9 @@ +import { ITag } from '../model'; +import { Store } from './store'; + +export interface ITagStore extends Store { + getTagsByType(type: string): Promise; + getTag(type: string, value: string): Promise; + createTag(tag: ITag): Promise; + bulkImport(tags: ITag[]): Promise; +} diff --git a/src/lib/types/stores/tag-type-store.ts b/src/lib/types/stores/tag-type-store.ts new file mode 100644 index 0000000000..56bfd5a915 --- /dev/null +++ b/src/lib/types/stores/tag-type-store.ts @@ -0,0 +1,13 @@ +import { Store } from './store'; + +export interface ITagType { + name: string; + description?: string; + icon?: string; +} + +export interface ITagTypeStore extends Store { + createTagType(newTagType: ITagType): Promise; + bulkImport(tagTypes: ITagType[]): Promise; + updateTagType(tagType: ITagType): Promise; +} diff --git a/src/lib/types/stores/user-feedback-store.ts b/src/lib/types/stores/user-feedback-store.ts new file mode 100644 index 0000000000..ae734d531c --- /dev/null +++ b/src/lib/types/stores/user-feedback-store.ts @@ -0,0 +1,20 @@ +import { Store } from './store'; + +export interface IUserFeedback { + neverShow: boolean; + feedbackId: string; + given?: Date; + userId: number; +} + +export interface IUserFeedbackKey { + userId: number; + feedbackId: string; +} + +export interface IUserFeedbackStore + extends Store { + getAllUserFeedback(userId: number): Promise; + getFeedback(userId: number, feedbackId: string): Promise; + updateFeedback(feedback: IUserFeedback): Promise; +} diff --git a/src/lib/types/stores/user-store.ts b/src/lib/types/stores/user-store.ts new file mode 100644 index 0000000000..f445eb4d7f --- /dev/null +++ b/src/lib/types/stores/user-store.ts @@ -0,0 +1,40 @@ +import { IUser } from '../user'; +import { Store } from './store'; + +export interface ICreateUser { + name?: string; + username?: string; + email?: string; + imageUrl?: string; +} + +export interface IUserLookup { + id?: number; + username?: string; + email?: string; +} + +export interface IUserSearch { + name?: string; + username?: string; + email: string; +} + +export interface IUserUpdateFields { + name?: string; + email?: string; +} + +export interface IUserStore extends Store { + update(id: number, fields: IUserUpdateFields): Promise; + insert(user: ICreateUser): Promise; + upsert(user: ICreateUser): Promise; + hasUser(idQuery: IUserLookup): Promise; + search(query: IUserSearch): Promise; + getAllWithId(userIdList: number[]): Promise; + getByQuery(idQuery: IUserLookup): Promise; + getPasswordHash(userId: number): Promise; + setPasswordHash(userId: number, passwordHash: string): Promise; + incLoginAttempts(user: IUser): Promise; + successfullyLogin(user: IUser): Promise; +} diff --git a/src/lib/types/user.ts b/src/lib/types/user.ts index 24fedc89fc..3a29517caa 100644 --- a/src/lib/types/user.ts +++ b/src/lib/types/user.ts @@ -18,7 +18,12 @@ export interface IUser { username?: string; email?: string; inviteLink?: string; + seenAt?: Date; createdAt: Date; + permissions: string[]; + loginAttempts: number; + isAPI: boolean; + imageUrl: string; } export default class User implements IUser { @@ -70,6 +75,7 @@ export default class User implements IUser { this.seenAt = seenAt; this.loginAttempts = loginAttempts; this.createdAt = createdAt; + this.imageUrl = this.generateImageUrl(); } generateImageUrl(): string { diff --git a/src/lib/util/metrics-helper.ts b/src/lib/util/metrics-helper.ts index f2fafbd488..c422bc5dcc 100644 --- a/src/lib/util/metrics-helper.ts +++ b/src/lib/util/metrics-helper.ts @@ -14,7 +14,7 @@ const wrapTimer: (EventEmitter, string, object) => (any) => any = ( args = {}, ) => { const t = timer.new(); - return data => { + return (data) => { args.time = t(); eventBus.emit(event, args); return data; diff --git a/src/lib/util/rewriteHTML.test.ts b/src/lib/util/rewriteHTML.test.ts index 2c1199e149..e75d9c7b31 100644 --- a/src/lib/util/rewriteHTML.test.ts +++ b/src/lib/util/rewriteHTML.test.ts @@ -29,30 +29,42 @@ const input = ` test('rewriteHTML substitutes meta tag with existing rewrite value', () => { const result = rewriteHTML(input, '/hosted'); - expect(result.includes(``)).toBe(true); + expect( + result.includes(''), + ).toBe(true); }); test('rewriteHTML substitutes meta tag with empty value', () => { const result = rewriteHTML(input, ''); - expect(result.includes(``)).toBe(true); + expect(result.includes('')).toBe( + true, + ); }); test('rewriteHTML substitutes asset paths correctly with baseUriPath', () => { const result = rewriteHTML(input, '/hosted'); - expect(result.includes( - ``, - )).toBe(true); - expect(result.includes( - ` `, - )).toBe(true); + expect( + result.includes( + '', + ), + ).toBe(true); + expect( + result.includes( + ' ', + ), + ).toBe(true); }); test('rewriteHTML substitutes asset paths correctly without baseUriPath', () => { const result = rewriteHTML(input, ''); - expect(result.includes( - ``, - )).toBe(true); - expect(result.includes( - ` `, - )).toBe(true); + expect( + result.includes( + '', + ), + ).toBe(true); + expect( + result.includes( + ' ', + ), + ).toBe(true); }); diff --git a/src/lib/util/timer.test.ts b/src/lib/util/timer.test.ts index aec38c77df..f225b6afc0 100644 --- a/src/lib/util/timer.test.ts +++ b/src/lib/util/timer.test.ts @@ -1,7 +1,7 @@ import timer from './timer'; function timeout(fn, ms): Promise { - return new Promise(resolve => + return new Promise((resolve) => setTimeout(() => { fn(); resolve(); diff --git a/src/lib/util/timer.ts b/src/lib/util/timer.ts index b690218854..400feef3e8 100644 --- a/src/lib/util/timer.ts +++ b/src/lib/util/timer.ts @@ -2,7 +2,7 @@ const NS_TO_S = 1e9; // seconds takes a tuple of [seconds, nanoseconds] // and returns the time in seconds -const seconds: (diff: [number, number]) => number = diff => +const seconds: (diff: [number, number]) => number = (diff) => diff[0] + diff[1] / NS_TO_S; const newTimer: () => () => number = () => { diff --git a/src/lib/util/version.ts b/src/lib/util/version.ts index 122aef39eb..35a283f9ec 100644 --- a/src/lib/util/version.ts +++ b/src/lib/util/version.ts @@ -1,5 +1,3 @@ -'use strict'; - // export module version require('pkginfo')(module, 'version'); diff --git a/src/migrations/20141020151056-initial-schema.js b/src/migrations/20141020151056-initial-schema.js index ca035cad02..a49f1fcebb 100644 --- a/src/migrations/20141020151056-initial-schema.js +++ b/src/migrations/20141020151056-initial-schema.js @@ -1,6 +1,6 @@ 'use strict'; -exports.up = function(db, callback) { +exports.up = function (db, callback) { db.runSql( ` CREATE TABLE strategies ( @@ -29,7 +29,7 @@ CREATE TABLE events ( ); }; -exports.down = function(db, callback) { +exports.down = function (db, callback) { db.runSql( ` DROP TABLE events; diff --git a/src/migrations/20141110144153-add-description-to-features.js b/src/migrations/20141110144153-add-description-to-features.js index cc6e93d26a..3cee235f27 100644 --- a/src/migrations/20141110144153-add-description-to-features.js +++ b/src/migrations/20141110144153-add-description-to-features.js @@ -1,9 +1,9 @@ 'use strict'; -exports.up = function(db, callback) { +exports.up = function (db, callback) { db.runSql('ALTER TABLE features ADD "description" text;', callback); }; -exports.down = function(db, callback) { +exports.down = function (db, callback) { db.runSql('ALTER TABLE features DROP COLUMN "description";', callback); }; diff --git a/src/migrations/20141117200435-add-parameters-template-to-strategies.js b/src/migrations/20141117200435-add-parameters-template-to-strategies.js index 05924341d5..a296109d2f 100644 --- a/src/migrations/20141117200435-add-parameters-template-to-strategies.js +++ b/src/migrations/20141117200435-add-parameters-template-to-strategies.js @@ -1,13 +1,13 @@ 'use strict'; -exports.up = function(db, callback) { +exports.up = function (db, callback) { db.runSql( 'ALTER TABLE strategies ADD "parameters_template" json;', callback, ); }; -exports.down = function(db, callback) { +exports.down = function (db, callback) { db.runSql( 'ALTER TABLE strategies DROP COLUMN "parameters_template";', callback, diff --git a/src/migrations/20141117202209-insert-default-strategy.js b/src/migrations/20141117202209-insert-default-strategy.js index 0687d75b07..94f94bd841 100644 --- a/src/migrations/20141117202209-insert-default-strategy.js +++ b/src/migrations/20141117202209-insert-default-strategy.js @@ -1,6 +1,6 @@ 'use strict'; -exports.up = function(db, callback) { +exports.up = function (db, callback) { db.runSql( ` INSERT INTO strategies(name, description) @@ -10,7 +10,7 @@ VALUES ('default', 'Default on/off strategy.'); ); }; -exports.down = function(db, callback) { +exports.down = function (db, callback) { db.runSql( ` DELETE FROM strategies where name='default';`, diff --git a/src/migrations/20141118071458-default-strategy-event.js b/src/migrations/20141118071458-default-strategy-event.js index 007b5314ee..94911d9bb1 100644 --- a/src/migrations/20141118071458-default-strategy-event.js +++ b/src/migrations/20141118071458-default-strategy-event.js @@ -1,6 +1,6 @@ 'use strict'; -exports.up = function(db, callback) { +exports.up = function (db, callback) { db.runSql( ` INSERT INTO events(type, created_by, data) @@ -10,7 +10,7 @@ VALUES ('strategy-created', 'migration', '{"name":"default","description":"Defau ); }; -exports.down = function(db, callback) { +exports.down = function (db, callback) { db.runSql( ` delete from events where type='strategy-created' and data->>'name' = 'default';`, diff --git a/src/migrations/20141215210141-005-archived-flag-to-features.js b/src/migrations/20141215210141-005-archived-flag-to-features.js index d416be25a9..a780e9f3f9 100644 --- a/src/migrations/20141215210141-005-archived-flag-to-features.js +++ b/src/migrations/20141215210141-005-archived-flag-to-features.js @@ -1,9 +1,9 @@ 'use strict'; -exports.up = function(db, callback) { +exports.up = function (db, callback) { db.runSql('ALTER TABLE features ADD archived integer DEFAULT 0;', callback); }; -exports.down = function(db, callback) { +exports.down = function (db, callback) { db.runSql('ALTER TABLE features DROP COLUMN "archived";', callback); }; diff --git a/src/migrations/20150210152531-006-rename-eventtype.js b/src/migrations/20150210152531-006-rename-eventtype.js index 79ddb195e8..992091e863 100644 --- a/src/migrations/20150210152531-006-rename-eventtype.js +++ b/src/migrations/20150210152531-006-rename-eventtype.js @@ -1,6 +1,6 @@ 'use strict'; -exports.up = function(db, callback) { +exports.up = function (db, callback) { db.runSql( ` UPDATE events SET type='feature-revived' WHERE type='feature-revive'; @@ -10,7 +10,7 @@ UPDATE events SET type='feature-archived' WHERE type='feature-archive'; ); }; -exports.down = function(db, callback) { +exports.down = function (db, callback) { db.runSql( ` UPDATE events SET type='feature-revive' WHERE type='feature-revived'; diff --git a/src/migrations/20160618193924-add-strategies-to-features.js b/src/migrations/20160618193924-add-strategies-to-features.js index 3dff4ed569..027cfc9fbd 100644 --- a/src/migrations/20160618193924-add-strategies-to-features.js +++ b/src/migrations/20160618193924-add-strategies-to-features.js @@ -1,6 +1,6 @@ 'use strict'; -exports.up = function(db, callback) { +exports.up = function (db, callback) { db.runSql( ` --create new strategies-column @@ -20,7 +20,7 @@ ALTER TABLE features DROP COLUMN "parameters"; ); }; -exports.down = function(db, callback) { +exports.down = function (db, callback) { db.runSql( ` --create old columns diff --git a/src/migrations/20161027134128-create-metrics.js b/src/migrations/20161027134128-create-metrics.js index 65423c08d7..54dccb1f6c 100644 --- a/src/migrations/20161027134128-create-metrics.js +++ b/src/migrations/20161027134128-create-metrics.js @@ -1,6 +1,6 @@ 'use strict'; -exports.up = function(db, callback) { +exports.up = function (db, callback) { db.runSql( ` CREATE TABLE client_metrics ( @@ -12,6 +12,6 @@ CREATE TABLE client_metrics ( ); }; -exports.down = function(db, callback) { +exports.down = function (db, callback) { db.runSql('DROP TABLE client_metrics;', callback); }; diff --git a/src/migrations/20161104074441-create-client-instances.js b/src/migrations/20161104074441-create-client-instances.js index 4446f096c8..514fc7e289 100644 --- a/src/migrations/20161104074441-create-client-instances.js +++ b/src/migrations/20161104074441-create-client-instances.js @@ -1,6 +1,6 @@ 'use strict'; -exports.up = function(db, callback) { +exports.up = function (db, callback) { db.runSql( ` CREATE TABLE client_instances ( @@ -14,6 +14,6 @@ exports.up = function(db, callback) { ); }; -exports.down = function(db, callback) { +exports.down = function (db, callback) { db.runSql('DROP TABLE client_instances;', callback); }; diff --git a/src/migrations/20161205203516-create-client-applications.js b/src/migrations/20161205203516-create-client-applications.js index 14e7b7962c..9ac82aa99f 100644 --- a/src/migrations/20161205203516-create-client-applications.js +++ b/src/migrations/20161205203516-create-client-applications.js @@ -2,7 +2,7 @@ 'use strict'; -exports.up = function(db, cb) { +exports.up = function (db, cb) { db.createTable( 'client_applications', { @@ -25,6 +25,6 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { +exports.down = function (db, cb) { return db.dropTable('client_applications', cb); }; diff --git a/src/migrations/20161212101749-better-strategy-parameter-definitions.js b/src/migrations/20161212101749-better-strategy-parameter-definitions.js index 02112fc811..c75313b31e 100644 --- a/src/migrations/20161212101749-better-strategy-parameter-definitions.js +++ b/src/migrations/20161212101749-better-strategy-parameter-definitions.js @@ -4,15 +4,15 @@ const async = require('async'); -exports.up = function(db, callback) { - const populateNewData = cb => { +exports.up = function (db, callback) { + const populateNewData = (cb) => { db.all( 'select name, parameters_template from strategies', (err, results) => { const updateSQL = results .map(({ name, parameters_template }) => { const parameters = []; - Object.keys(parameters_template || {}).forEach(p => { + Object.keys(parameters_template || {}).forEach((p) => { parameters.push({ name: p, type: parameters_template[p], @@ -23,7 +23,7 @@ exports.up = function(db, callback) { return { name, parameters }; }) .map( - strategy => ` + (strategy) => ` UPDATE strategies SET parameters='${JSON.stringify(strategy.parameters)}' WHERE name='${strategy.name}';`, @@ -45,20 +45,20 @@ exports.up = function(db, callback) { ); }; -exports.down = function(db, callback) { - const populateOldData = cb => { +exports.down = function (db, callback) { + const populateOldData = (cb) => { db.all('select name, parameters from strategies', (err, results) => { const updateSQL = results .map(({ name, parameters }) => { const parameters_template = {}; - parameters.forEach(p => { + parameters.forEach((p) => { parameters_template[p.name] = p.type; }); return { name, parameters_template }; }) .map( - strategy => ` + (strategy) => ` UPDATE strategies SET parameters_template='${JSON.stringify( strategy.parameters_template, diff --git a/src/migrations/20170211085502-built-in-strategies.js b/src/migrations/20170211085502-built-in-strategies.js index 32972776ef..8040b42511 100644 --- a/src/migrations/20170211085502-built-in-strategies.js +++ b/src/migrations/20170211085502-built-in-strategies.js @@ -2,7 +2,7 @@ const async = require('async'); -exports.up = function(db, cb) { +exports.up = function (db, cb) { async.series( [ db.addColumn.bind(db, 'strategies', 'built_in', { @@ -18,6 +18,6 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { +exports.down = function (db, cb) { return db.removeColumn('strategies', 'built_in', cb); }; diff --git a/src/migrations/20170211090541-add-default-strategies.js b/src/migrations/20170211090541-add-default-strategies.js index ee04ceb587..39b517a374 100644 --- a/src/migrations/20170211090541-add-default-strategies.js +++ b/src/migrations/20170211090541-add-default-strategies.js @@ -43,8 +43,8 @@ function removeStrategySQL(strategy) { WHERE name = '${strategy.name}' AND built_in = 1`; } -exports.up = function(db, callback) { - const insertStrategies = strategies.map(s => cb => { +exports.up = function (db, callback) { + const insertStrategies = strategies.map((s) => (cb) => { async.series( [ db.runSql.bind(db, insertEventsSQL(s)), @@ -56,10 +56,10 @@ exports.up = function(db, callback) { async.series(insertStrategies, callback); }; -exports.down = function(db, callback) { +exports.down = function (db, callback) { const removeStrategies = strategies - .filter(s => s.name !== 'default') - .map(s => cb => { + .filter((s) => s.name !== 'default') + .map((s) => (cb) => { async.series( [ db.runSql.bind(db, removeEventsSQL(s)), diff --git a/src/migrations/20170306233934-timestamp-with-tz.js b/src/migrations/20170306233934-timestamp-with-tz.js index 4fec7b7254..2eb00f6b1b 100644 --- a/src/migrations/20170306233934-timestamp-with-tz.js +++ b/src/migrations/20170306233934-timestamp-with-tz.js @@ -1,6 +1,6 @@ 'use strict'; -exports.up = function(db, callback) { +exports.up = function (db, callback) { db.runSql( ` ALTER TABLE events ALTER COLUMN created_at TYPE TIMESTAMP WITH TIME ZONE; @@ -17,6 +17,6 @@ ALTER TABLE client_metrics ALTER COLUMN created_at TYPE TIMESTAMP WITH TIME ZONE ); }; -exports.down = function(db, callback) { +exports.down = function (db, callback) { callback(); }; diff --git a/src/migrations/20170628205541-add-sdk-version-to-client-instances.js b/src/migrations/20170628205541-add-sdk-version-to-client-instances.js index 5b91435dcf..4170f387c5 100644 --- a/src/migrations/20170628205541-add-sdk-version-to-client-instances.js +++ b/src/migrations/20170628205541-add-sdk-version-to-client-instances.js @@ -1,13 +1,13 @@ 'use strict'; -exports.up = function(db, callback) { +exports.up = function (db, callback) { db.runSql( 'ALTER TABLE client_instances ADD "sdk_version" varchar(255);', callback, ); }; -exports.down = function(db, callback) { +exports.down = function (db, callback) { db.runSql( 'ALTER TABLE client_instances DROP COLUMN "sdk_version";', callback, diff --git a/src/migrations/20190123204125-add-variants-to-features.js b/src/migrations/20190123204125-add-variants-to-features.js index f55ac57cc7..d33357a87e 100644 --- a/src/migrations/20190123204125-add-variants-to-features.js +++ b/src/migrations/20190123204125-add-variants-to-features.js @@ -1,6 +1,6 @@ 'use strict'; -exports.up = function(db, callback) { +exports.up = function (db, callback) { db.runSql( ` ALTER TABLE features ADD "variants" json; @@ -10,6 +10,6 @@ exports.up = function(db, callback) { ); }; -exports.down = function(db, callback) { +exports.down = function (db, callback) { db.runSql('ALTER TABLE features DROP COLUMN "variants";', callback); }; diff --git a/src/migrations/20191023184858-flexible-rollout-strategy.js b/src/migrations/20191023184858-flexible-rollout-strategy.js index 67d0197c67..476b20d636 100644 --- a/src/migrations/20191023184858-flexible-rollout-strategy.js +++ b/src/migrations/20191023184858-flexible-rollout-strategy.js @@ -43,7 +43,7 @@ function removeStrategySQL(strategy) { WHERE name = '${strategy.name}' AND built_in = 1`; } -exports.up = function(db, callback) { +exports.up = function (db, callback) { async.series( [ db.runSql.bind(db, insertEventsSQL(flexibleRollout)), @@ -53,7 +53,7 @@ exports.up = function(db, callback) { ); }; -exports.down = function(db, callback) { +exports.down = function (db, callback) { async.series( [ db.runSql.bind(db, removeEventsSQL(flexibleRollout)), diff --git a/src/migrations/20200102184820-create-context-fields.js b/src/migrations/20200102184820-create-context-fields.js index caa5ac92ce..18b7aaa864 100644 --- a/src/migrations/20200102184820-create-context-fields.js +++ b/src/migrations/20200102184820-create-context-fields.js @@ -4,7 +4,7 @@ const async = require('async'); -exports.up = function(db, cb) { +exports.up = function (db, cb) { async.series( [ db.createTable.bind(db, 'context_fields', { @@ -33,6 +33,6 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { +exports.down = function (db, cb) { return db.dropTable('context_fields', cb); }; diff --git a/src/migrations/20200227202711-settings.js b/src/migrations/20200227202711-settings.js index 2fce551934..22e0746e7d 100644 --- a/src/migrations/20200227202711-settings.js +++ b/src/migrations/20200227202711-settings.js @@ -2,7 +2,7 @@ 'use strict'; -exports.up = function(db, cb) { +exports.up = function (db, cb) { return db.createTable( 'settings', { @@ -18,6 +18,6 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { +exports.down = function (db, cb) { return db.dropTable('settings', cb); }; diff --git a/src/migrations/20200329191251-settings-secret.js b/src/migrations/20200329191251-settings-secret.js index 032e7da479..1d79d52f7d 100644 --- a/src/migrations/20200329191251-settings-secret.js +++ b/src/migrations/20200329191251-settings-secret.js @@ -6,7 +6,7 @@ const crypto = require('crypto'); const settingsName = 'unleash.secret'; -exports.up = function(db, cb) { +exports.up = function (db, cb) { const secret = crypto.randomBytes(20).toString('hex'); db.runSql( @@ -17,6 +17,6 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { +exports.down = function (db, cb) { db.runSql(`DELETE FROM settings WHERE name = '${settingsName}'`, cb); }; diff --git a/src/migrations/20200416201319-create-users.js b/src/migrations/20200416201319-create-users.js index 9d6cbbfaa2..49fb6b41b9 100644 --- a/src/migrations/20200416201319-create-users.js +++ b/src/migrations/20200416201319-create-users.js @@ -2,7 +2,7 @@ 'use strict'; -exports.up = function(db, cb) { +exports.up = function (db, cb) { return db.createTable( 'users', { @@ -26,6 +26,6 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { +exports.down = function (db, cb) { return db.dropTable('users', cb); }; diff --git a/src/migrations/20200429175747-users-settings.js b/src/migrations/20200429175747-users-settings.js index ceaed7e339..2fcd5102b4 100644 --- a/src/migrations/20200429175747-users-settings.js +++ b/src/migrations/20200429175747-users-settings.js @@ -1,6 +1,6 @@ 'use strict'; -exports.up = function(db, callback) { +exports.up = function (db, callback) { db.runSql( ` ALTER TABLE users ADD "settings" json; @@ -12,7 +12,7 @@ exports.up = function(db, callback) { ); }; -exports.down = function(db, callback) { +exports.down = function (db, callback) { db.runSql( ` ALTER TABLE users DROP COLUMN "settings"; diff --git a/src/migrations/20200805091409-add-feature-toggle-type.js b/src/migrations/20200805091409-add-feature-toggle-type.js index 6dc9587b98..97d3ac6347 100644 --- a/src/migrations/20200805091409-add-feature-toggle-type.js +++ b/src/migrations/20200805091409-add-feature-toggle-type.js @@ -4,7 +4,7 @@ const async = require('async'); -exports.up = function(db, cb) { +exports.up = function (db, cb) { async.series( [ db.createTable.bind(db, 'feature_types', { @@ -33,6 +33,6 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { +exports.down = function (db, cb) { return db.dropTable('feature_types', cb); }; diff --git a/src/migrations/20200805094311-add-feature-type-to-features.js b/src/migrations/20200805094311-add-feature-type-to-features.js index 13c6fbeeab..5f085d020d 100644 --- a/src/migrations/20200805094311-add-feature-type-to-features.js +++ b/src/migrations/20200805094311-add-feature-type-to-features.js @@ -1,6 +1,6 @@ 'use strict'; -exports.up = function(db, cb) { +exports.up = function (db, cb) { return db.addColumn( 'features', 'type', @@ -12,6 +12,6 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { +exports.down = function (db, cb) { return db.removeColumn('features', 'type', cb); }; diff --git a/src/migrations/20200806091734-add-stale-flag-to-features.js b/src/migrations/20200806091734-add-stale-flag-to-features.js index 944245f8c8..91b54755a6 100644 --- a/src/migrations/20200806091734-add-stale-flag-to-features.js +++ b/src/migrations/20200806091734-add-stale-flag-to-features.js @@ -1,6 +1,6 @@ 'use strict'; -exports.up = function(db, cb) { +exports.up = function (db, cb) { return db.addColumn( 'features', 'stale', @@ -12,6 +12,6 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { +exports.down = function (db, cb) { return db.removeColumn('features', 'stale', cb); }; diff --git a/src/migrations/20200810200901-add-created-at-to-feature-types.js b/src/migrations/20200810200901-add-created-at-to-feature-types.js index 9489aa8f90..b1a7b16ea8 100644 --- a/src/migrations/20200810200901-add-created-at-to-feature-types.js +++ b/src/migrations/20200810200901-add-created-at-to-feature-types.js @@ -1,12 +1,12 @@ 'use strict'; -exports.up = function(db, callback) { +exports.up = function (db, callback) { db.runSql( 'ALTER TABLE feature_types ADD "created_at" TIMESTAMP WITH TIME ZONE default now();', callback, ); }; -exports.down = function(db, callback) { +exports.down = function (db, callback) { db.runSql('ALTER TABLE feature_types DROP COLUMN "created_at";', callback); }; diff --git a/src/migrations/20200928194947-add-projects.js b/src/migrations/20200928194947-add-projects.js index 526756d3fb..288abc2ff1 100644 --- a/src/migrations/20200928194947-add-projects.js +++ b/src/migrations/20200928194947-add-projects.js @@ -4,7 +4,7 @@ const async = require('async'); -exports.up = function(db, cb) { +exports.up = function (db, cb) { async.series( [ db.createTable.bind(db, 'projects', { @@ -29,6 +29,6 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { +exports.down = function (db, cb) { return db.dropTable('projects', cb); }; diff --git a/src/migrations/20200928195238-add-project-id-to-features.js b/src/migrations/20200928195238-add-project-id-to-features.js index e113023a84..51ac663b12 100644 --- a/src/migrations/20200928195238-add-project-id-to-features.js +++ b/src/migrations/20200928195238-add-project-id-to-features.js @@ -1,6 +1,6 @@ 'use strict'; -exports.up = function(db, cb) { +exports.up = function (db, cb) { return db.addColumn( 'features', 'project', @@ -12,6 +12,6 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { +exports.down = function (db, cb) { return db.removeColumn('features', 'project', cb); }; diff --git a/src/migrations/20201216140726-add-last-seen-to-features.js b/src/migrations/20201216140726-add-last-seen-to-features.js index 747621f8d7..d861af100b 100644 --- a/src/migrations/20201216140726-add-last-seen-to-features.js +++ b/src/migrations/20201216140726-add-last-seen-to-features.js @@ -1,10 +1,10 @@ -exports.up = function(db, callback) { +exports.up = function (db, callback) { db.runSql( 'ALTER TABLE features ADD "last_seen_at" TIMESTAMP WITH TIME ZONE;', callback, ); }; -exports.down = function(db, cb) { +exports.down = function (db, cb) { return db.removeColumn('features', 'last_seen_at', cb); }; diff --git a/src/migrations/20210105083014-add-tag-and-tag-types.js b/src/migrations/20210105083014-add-tag-and-tag-types.js index 083083e810..2aff17878a 100644 --- a/src/migrations/20210105083014-add-tag-and-tag-types.js +++ b/src/migrations/20210105083014-add-tag-and-tag-types.js @@ -1,4 +1,4 @@ -exports.up = function(db, cb) { +exports.up = function (db, cb) { db.runSql( `CREATE TABLE IF NOT EXISTS tag_types ( @@ -30,7 +30,7 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { +exports.down = function (db, cb) { db.runSql( `DROP TABLE feature_tag; DROP TABLE tags; diff --git a/src/migrations/20210119084617-add-addon-table.js b/src/migrations/20210119084617-add-addon-table.js index d802d61202..4b311e5591 100644 --- a/src/migrations/20210119084617-add-addon-table.js +++ b/src/migrations/20210119084617-add-addon-table.js @@ -1,4 +1,4 @@ -exports.up = function(db, cb) { +exports.up = function (db, cb) { db.runSql( `CREATE TABLE IF NOT EXISTS addons ( @@ -15,6 +15,6 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { +exports.down = function (db, cb) { db.runSql('DROP TABLE addons;', cb); }; diff --git a/src/migrations/20210121115438-add-deprecated-column-to-strategies.js b/src/migrations/20210121115438-add-deprecated-column-to-strategies.js index 3f22836e74..fc18e428bf 100644 --- a/src/migrations/20210121115438-add-deprecated-column-to-strategies.js +++ b/src/migrations/20210121115438-add-deprecated-column-to-strategies.js @@ -1,6 +1,6 @@ 'use strict'; -exports.up = function(db, cb) { +exports.up = function (db, cb) { db.runSql( ` ALTER TABLE strategies ADD COLUMN deprecated boolean default false @@ -9,7 +9,7 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { +exports.down = function (db, cb) { db.runSql('ALTER TABLE strategies DROP COLUMN deprecated', cb); }; diff --git a/src/migrations/20210127094440-add-tags-column-to-events.js b/src/migrations/20210127094440-add-tags-column-to-events.js index 4883a095ef..f45bfefad1 100644 --- a/src/migrations/20210127094440-add-tags-column-to-events.js +++ b/src/migrations/20210127094440-add-tags-column-to-events.js @@ -1,6 +1,6 @@ 'use strict'; -exports.up = function(db, cb) { +exports.up = function (db, cb) { db.runSql( ` ALTER TABLE events ADD COLUMN IF NOT EXISTS tags json DEFAULT '[]' @@ -8,7 +8,7 @@ exports.up = function(db, cb) { cb, ); }; -exports.down = function(db, cb) { +exports.down = function (db, cb) { db.runSql( ` ALTER TABLE events DROP COLUMN IF EXISTS tags; diff --git a/src/migrations/20210208203708-add-stickiness-to-context.js b/src/migrations/20210208203708-add-stickiness-to-context.js index 5a4bef060f..6c66550e9c 100644 --- a/src/migrations/20210208203708-add-stickiness-to-context.js +++ b/src/migrations/20210208203708-add-stickiness-to-context.js @@ -1,6 +1,6 @@ 'use strict'; -exports.up = function(db, cb) { +exports.up = function (db, cb) { db.runSql( ` ALTER TABLE context_fields ADD COLUMN IF NOT EXISTS stickiness boolean DEFAULT false @@ -8,7 +8,7 @@ exports.up = function(db, cb) { cb, ); }; -exports.down = function(db, cb) { +exports.down = function (db, cb) { db.runSql( ` ALTER TABLE context_fields DROP COLUMN IF EXISTS stickiness; diff --git a/src/migrations/20210212114759-add-session-table.js b/src/migrations/20210212114759-add-session-table.js index 790d0d0fb9..e44333bcd6 100644 --- a/src/migrations/20210212114759-add-session-table.js +++ b/src/migrations/20210212114759-add-session-table.js @@ -1,4 +1,4 @@ -exports.up = function(db, cb) { +exports.up = function (db, cb) { db.runSql( ` CREATE TABLE unleash_session ( @@ -13,7 +13,7 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { +exports.down = function (db, cb) { db.runSql( ` DROP INDEX idx_unleash_session_expired; diff --git a/src/migrations/20210217195834-rbac-tables.js b/src/migrations/20210217195834-rbac-tables.js index 99329dee62..ed3825c7e5 100644 --- a/src/migrations/20210217195834-rbac-tables.js +++ b/src/migrations/20210217195834-rbac-tables.js @@ -1,4 +1,4 @@ -exports.up = function(db, cb) { +exports.up = function (db, cb) { db.runSql( `CREATE TABLE IF NOT EXISTS roles ( @@ -69,7 +69,7 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { +exports.down = function (db, cb) { db.runSql( ` DROP TABLE role_user; diff --git a/src/migrations/20210218090213-generate-server-identifier.js b/src/migrations/20210218090213-generate-server-identifier.js index fe25abd122..91cba7c75e 100644 --- a/src/migrations/20210218090213-generate-server-identifier.js +++ b/src/migrations/20210218090213-generate-server-identifier.js @@ -2,7 +2,7 @@ const { v4: uuidv4 } = require('uuid'); -exports.up = function(db, cb) { +exports.up = function (db, cb) { const instanceId = uuidv4(); db.runSql( ` @@ -12,7 +12,7 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { +exports.down = function (db, cb) { db.runSql( ` DELETE FROM settings WHERE name = 'instanceInfo' diff --git a/src/migrations/20210302080040-add-pk-to-client-instances.js b/src/migrations/20210302080040-add-pk-to-client-instances.js index 210f674638..b13e2ff41c 100644 --- a/src/migrations/20210302080040-add-pk-to-client-instances.js +++ b/src/migrations/20210302080040-add-pk-to-client-instances.js @@ -1,6 +1,6 @@ 'use strict'; -exports.up = function(db, cb) { +exports.up = function (db, cb) { db.runSql( ` DELETE FROM client_instances a USING client_instances b WHERE a.app_name = b.app_name AND a.instance_id = b.instance_id AND a.created_at < b.created_at; @@ -10,7 +10,7 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { +exports.down = function (db, cb) { db.runSql( ` ALTER TABLE client_instances DROP CONSTRAINT client_instances_pkey; diff --git a/src/migrations/20210304115810-change-default-timestamp-to-now.js b/src/migrations/20210304115810-change-default-timestamp-to-now.js index e1dfe617e1..7974f92ee1 100644 --- a/src/migrations/20210304115810-change-default-timestamp-to-now.js +++ b/src/migrations/20210304115810-change-default-timestamp-to-now.js @@ -1,6 +1,6 @@ 'use strict'; -exports.up = function(db, cb) { +exports.up = function (db, cb) { db.runSql( ` ALTER TABLE client_applications ALTER COLUMN created_at SET DEFAULT now(); @@ -14,7 +14,7 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { +exports.down = function (db, cb) { db.runSql( ` ALTER TABLE client_applications ALTER COLUMN created_at SET DEFAULT 'now()'; diff --git a/src/migrations/20210304141005-add-announce-field-to-application.js b/src/migrations/20210304141005-add-announce-field-to-application.js index 36e3f5e410..cb2759e1a4 100644 --- a/src/migrations/20210304141005-add-announce-field-to-application.js +++ b/src/migrations/20210304141005-add-announce-field-to-application.js @@ -1,6 +1,6 @@ 'use strict'; -exports.up = function(db, cb) { +exports.up = function (db, cb) { db.runSql( ` ALTER TABLE client_applications ADD COLUMN announced boolean DEFAULT false; @@ -10,7 +10,7 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { +exports.down = function (db, cb) { db.runSql( ` ALTER TABLE client_applications DROP COLUMN announced; diff --git a/src/migrations/20210304150739-add-created-by-to-application.js b/src/migrations/20210304150739-add-created-by-to-application.js index 1505d84b50..8c3fa11ee5 100644 --- a/src/migrations/20210304150739-add-created-by-to-application.js +++ b/src/migrations/20210304150739-add-created-by-to-application.js @@ -1,6 +1,6 @@ 'use strict'; -exports.up = function(db, cb) { +exports.up = function (db, cb) { db.runSql( ` ALTER TABLE client_applications ADD COLUMN created_by TEXT; @@ -9,7 +9,7 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { +exports.down = function (db, cb) { db.runSql('ALTER TABLE client_applications DROP COLUMN created_by;', cb); }; diff --git a/src/migrations/20210322104356-api-tokens-table.js b/src/migrations/20210322104356-api-tokens-table.js index fa6099e65b..b9a1f6a288 100644 --- a/src/migrations/20210322104356-api-tokens-table.js +++ b/src/migrations/20210322104356-api-tokens-table.js @@ -1,6 +1,6 @@ 'use strict'; -exports.up = function(db, cb) { +exports.up = function (db, cb) { db.runSql( `CREATE TABLE IF NOT EXISTS api_tokens ( @@ -16,6 +16,6 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { +exports.down = function (db, cb) { db.runSql('DROP TABLE IF EXISTS api_tokens;', cb); }; diff --git a/src/migrations/20210322104357-api-tokens-convert-enterprise.js b/src/migrations/20210322104357-api-tokens-convert-enterprise.js index 5e7eff2f45..5a7e52d250 100644 --- a/src/migrations/20210322104357-api-tokens-convert-enterprise.js +++ b/src/migrations/20210322104357-api-tokens-convert-enterprise.js @@ -4,20 +4,22 @@ const async = require('async'); const settingsId = 'unleash.enterprise.api.keys'; -const toApiToken = legacyToken => ({ +const toApiToken = (legacyToken) => ({ secret: legacyToken.key, username: legacyToken.username, createdAt: legacyToken.created || new Date(), - type: legacyToken.priviliges.some(n => n === 'ADMIN') ? 'admin' : 'client', + type: legacyToken.priviliges.some((n) => n === 'ADMIN') + ? 'admin' + : 'client', }); -exports.up = function(db, cb) { +exports.up = function (db, cb) { db.runSql( `SELECT * from settings where name = '${settingsId}';`, (err, results) => { if (results.rowCount === 1) { const legacyTokens = results.rows[0].content.keys; - const inserts = legacyTokens.map(toApiToken).map(t => + const inserts = legacyTokens.map(toApiToken).map((t) => db.runSql.bind( db, `INSERT INTO api_tokens (secret, username, type, created_at) @@ -34,6 +36,6 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { +exports.down = function (db, cb) { cb(); }; diff --git a/src/migrations/20210323073508-reset-application-announcements.js b/src/migrations/20210323073508-reset-application-announcements.js index f92a674364..f75e5b949b 100644 --- a/src/migrations/20210323073508-reset-application-announcements.js +++ b/src/migrations/20210323073508-reset-application-announcements.js @@ -1,6 +1,6 @@ 'use strict'; -exports.up = function(db, cb) { +exports.up = function (db, cb) { db.runSql( ` DELETE FROM events WHERE type = 'application-created'; @@ -10,6 +10,6 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { +exports.down = function (db, cb) { cb(); }; diff --git a/src/migrations/20210409120136-create-reset-token-table.js b/src/migrations/20210409120136-create-reset-token-table.js index d0881dd4fc..649d550ad0 100644 --- a/src/migrations/20210409120136-create-reset-token-table.js +++ b/src/migrations/20210409120136-create-reset-token-table.js @@ -1,4 +1,4 @@ -exports.up = function(db, cb) { +exports.up = function (db, cb) { db.runSql( ` CREATE TABLE IF NOT EXISTS reset_tokens @@ -17,7 +17,7 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { +exports.down = function (db, cb) { db.runSql('DROP TABLE reset_tokens;', cb); }; diff --git a/src/migrations/20210414141220-fix-misspellings-in-role-descriptions.js b/src/migrations/20210414141220-fix-misspellings-in-role-descriptions.js index 3717187638..1e88e726ac 100644 --- a/src/migrations/20210414141220-fix-misspellings-in-role-descriptions.js +++ b/src/migrations/20210414141220-fix-misspellings-in-role-descriptions.js @@ -1,4 +1,4 @@ -exports.up = function(db, cb) { +exports.up = function (db, cb) { db.runSql( ` UPDATE roles SET description = 'As an Editor you have access to most features in Unleash, but you can not manage users and roles in the global scope. If you create a project, you will become a project owner and receive superuser rights within the context of that project.' WHERE name = 'Regular'; @@ -7,6 +7,6 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { +exports.down = function (db, cb) { db.runSql('', cb); }; diff --git a/src/migrations/20210415173116-rbac-rename-roles.js b/src/migrations/20210415173116-rbac-rename-roles.js index 85c7e207f0..df0e18da9b 100644 --- a/src/migrations/20210415173116-rbac-rename-roles.js +++ b/src/migrations/20210415173116-rbac-rename-roles.js @@ -1,15 +1,12 @@ 'use strict'; const DESCRIPTION = { - EDITOR: - 'Users with this role have access most features in Unleash, but can not manage users and roles in the global scope. If a user with a global regular role creates a project, they will become a project admin and receive superuser rights within the context of that project.', - OWNER: - 'Users with this role have full control over the project, and can add and manage other users within the project context, manage feature toggles within the project, and control advanced project features like archiving and deleting the project.', - MEMBER: - 'Users with this role within a project are allowed to view, create and update feature toggles, but have limited permissions in regards to managing the projects user access and can not archive or delete the project.', + EDITOR: 'Users with this role have access most features in Unleash, but can not manage users and roles in the global scope. If a user with a global regular role creates a project, they will become a project admin and receive superuser rights within the context of that project.', + OWNER: 'Users with this role have full control over the project, and can add and manage other users within the project context, manage feature toggles within the project, and control advanced project features like archiving and deleting the project.', + MEMBER: 'Users with this role within a project are allowed to view, create and update feature toggles, but have limited permissions in regards to managing the projects user access and can not archive or delete the project.', }; -exports.up = function(db, cb) { +exports.up = function (db, cb) { db.runSql( ` UPDATE roles set name = 'Editor', description = '${DESCRIPTION.EDITOR}' where name = 'Regular' AND type = 'root'; @@ -21,7 +18,7 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { +exports.down = function (db, cb) { db.runSql( ` UPDATE roles set name = 'Regular' where name = 'Editor' AND type = 'root'; diff --git a/src/migrations/20210421133845-add-sort-order-to-strategies.js b/src/migrations/20210421133845-add-sort-order-to-strategies.js index b7185f636c..cc5b390615 100644 --- a/src/migrations/20210421133845-add-sort-order-to-strategies.js +++ b/src/migrations/20210421133845-add-sort-order-to-strategies.js @@ -1,6 +1,6 @@ 'use strict'; -exports.up = function(db, cb) { +exports.up = function (db, cb) { db.runSql( ` ALTER TABLE strategies ADD COLUMN sort_order integer DEFAULT 9999; @@ -14,6 +14,6 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { - db.runSql(`ALTER TABLE strategies DROP COLUMN sort_order;`, cb); +exports.down = function (db, cb) { + db.runSql('ALTER TABLE strategies DROP COLUMN sort_order;', cb); }; diff --git a/src/migrations/20210421135405-add-display-name-and-update-description-for-strategies.js b/src/migrations/20210421135405-add-display-name-and-update-description-for-strategies.js index eae9a51de0..1ddbfefcd6 100644 --- a/src/migrations/20210421135405-add-display-name-and-update-description-for-strategies.js +++ b/src/migrations/20210421135405-add-display-name-and-update-description-for-strategies.js @@ -1,6 +1,6 @@ 'use strict'; -exports.up = function(db, cb) { +exports.up = function (db, cb) { db.runSql( ` ALTER TABLE strategies ADD COLUMN display_name text; @@ -14,6 +14,6 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { - db.runSql(`ALTER TABLE strategies DROP COLUMN display_name;`, cb); +exports.down = function (db, cb) { + db.runSql('ALTER TABLE strategies DROP COLUMN display_name;', cb); }; diff --git a/src/migrations/20210423103647-lowercase-all-emails.js b/src/migrations/20210423103647-lowercase-all-emails.js index bbd9e04065..a561b2d118 100644 --- a/src/migrations/20210423103647-lowercase-all-emails.js +++ b/src/migrations/20210423103647-lowercase-all-emails.js @@ -1,4 +1,4 @@ -exports.up = function(db, cb) { +exports.up = function (db, cb) { db.runSql( ` DELETE FROM users WHERE id IN @@ -12,6 +12,6 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { +exports.down = function (db, cb) { db.runSql('', cb); }; diff --git a/src/migrations/20210428062103-user-permission-to-rbac.js b/src/migrations/20210428062103-user-permission-to-rbac.js index 41fd24259d..f146443cba 100644 --- a/src/migrations/20210428062103-user-permission-to-rbac.js +++ b/src/migrations/20210428062103-user-permission-to-rbac.js @@ -12,13 +12,13 @@ function resolveRoleName(permissions) { return 'Editor'; } -exports.up = function(db, cb) { +exports.up = function (db, cb) { db.runSql( - `SELECT id, permissions from users WHERE id NOT IN (select user_id from role_user);`, + 'SELECT id, permissions from users WHERE id NOT IN (select user_id from role_user);', (err, results) => { if (results.rowCount > 0) { const users = results.rows; - const insertRootRole = users.map(u => { + const insertRootRole = users.map((u) => { const roleName = resolveRoleName(u.permissions); return db.runSql.bind( db, @@ -36,7 +36,7 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { +exports.down = function (db, cb) { // We can't just remove roles for users as we don't know if there has been any manual additions. cb(); }; diff --git a/src/migrations/20210428103923-onboard-projects-to-rbac.js b/src/migrations/20210428103923-onboard-projects-to-rbac.js index 1f8d13abf4..7790de364e 100644 --- a/src/migrations/20210428103923-onboard-projects-to-rbac.js +++ b/src/migrations/20210428103923-onboard-projects-to-rbac.js @@ -1,18 +1,16 @@ const async = require('async'); const DESCRIPTION = { - OWNER: - 'Users with this role have full control over the project, and can add and manage other users within the project context, manage feature toggles within the project, and control advanced project features like archiving and deleting the project.', - MEMBER: - 'Users with this role within a project are allowed to view, create and update feature toggles, but have limited permissions in regards to managing the projects user access and can not archive or delete the project.', + OWNER: 'Users with this role have full control over the project, and can add and manage other users within the project context, manage feature toggles within the project, and control advanced project features like archiving and deleting the project.', + MEMBER: 'Users with this role within a project are allowed to view, create and update feature toggles, but have limited permissions in regards to managing the projects user access and can not archive or delete the project.', }; -exports.up = function(db, cb) { +exports.up = function (db, cb) { db.runSql( - `SELECT id AS name from projects WHERE id NOT IN (SELECT DISTINCT project FROM roles WHERE project IS NOT null)`, + 'SELECT id AS name from projects WHERE id NOT IN (SELECT DISTINCT project FROM roles WHERE project IS NOT null)', (err, results) => { if (results && results.rowCount > 0) { const projects = results.rows; - const createProjectRoles = projects.map(p => + const createProjectRoles = projects.map((p) => db.runSql.bind( db, ` @@ -55,6 +53,6 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { +exports.down = function (db, cb) { cb(); // Can't really roll this back since more roles could have been added afterwards }; diff --git a/src/migrations/20210504101429-deprecate-strategies.js b/src/migrations/20210504101429-deprecate-strategies.js index e78fb889ef..d645f8c495 100644 --- a/src/migrations/20210504101429-deprecate-strategies.js +++ b/src/migrations/20210504101429-deprecate-strategies.js @@ -1,4 +1,4 @@ -exports.up = function(db, cb) { +exports.up = function (db, cb) { db.runSql( ` UPDATE strategies @@ -10,7 +10,7 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { +exports.down = function (db, cb) { db.runSql( ` UPDATE strategies diff --git a/src/migrations/20210520171325-update-role-descriptions.js b/src/migrations/20210520171325-update-role-descriptions.js index bf7a753ea8..379a02614e 100644 --- a/src/migrations/20210520171325-update-role-descriptions.js +++ b/src/migrations/20210520171325-update-role-descriptions.js @@ -1,15 +1,12 @@ 'use strict'; const DESCRIPTION = { - EDITOR: - 'Users with the editor role have access to most features in Unleash, but can not manage users and roles in the global scope. Editors will be added as project owner when creating projects and get superuser rights within the context of these projects.', - ADMIN: - 'Users with the global admin role have superuser access to Unleash and can perform any operation within the unleash platform.', - VIEWER: - 'Users with this role can only read root resources in Unleash. The viewer can be added to specific projects as project member.', + EDITOR: 'Users with the editor role have access to most features in Unleash, but can not manage users and roles in the global scope. Editors will be added as project owner when creating projects and get superuser rights within the context of these projects.', + ADMIN: 'Users with the global admin role have superuser access to Unleash and can perform any operation within the unleash platform.', + VIEWER: 'Users with this role can only read root resources in Unleash. The viewer can be added to specific projects as project member.', }; -exports.up = function(db, cb) { +exports.up = function (db, cb) { db.runSql( ` UPDATE roles set description = '${DESCRIPTION.EDITOR}' where name = 'Editor' AND type = 'root'; @@ -20,7 +17,7 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { +exports.down = function (db, cb) { db.runSql( ` `, diff --git a/src/migrations/20210602115555-create-feedback-table.js b/src/migrations/20210602115555-create-feedback-table.js index c7152bacb7..a436ef1851 100644 --- a/src/migrations/20210602115555-create-feedback-table.js +++ b/src/migrations/20210602115555-create-feedback-table.js @@ -1,6 +1,6 @@ 'use strict'; -exports.up = function(db, cb) { +exports.up = function (db, cb) { db.runSql( ` CREATE TABLE IF NOT EXISTS user_feedback @@ -15,7 +15,7 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { +exports.down = function (db, cb) { db.runSql( ` DROP INDEX user_feedback_user_id_idx; diff --git a/src/migrations/20210610085817-features-strategies-table.js b/src/migrations/20210610085817-features-strategies-table.js index e8fc4b16b1..b0b74ce236 100644 --- a/src/migrations/20210610085817-features-strategies-table.js +++ b/src/migrations/20210610085817-features-strategies-table.js @@ -1,6 +1,6 @@ 'use strict'; -exports.up = function(db, cb) { +exports.up = function (db, cb) { db.runSql( ` CREATE TABLE IF NOT EXISTS environments ( @@ -35,7 +35,7 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { +exports.down = function (db, cb) { db.runSql( ` DROP TABLE feature_strategies; diff --git a/src/migrations/20210615115226-migrate-strategies-to-feature-strategies.js b/src/migrations/20210615115226-migrate-strategies-to-feature-strategies.js index e6bc720359..543fd98856 100644 --- a/src/migrations/20210615115226-migrate-strategies-to-feature-strategies.js +++ b/src/migrations/20210615115226-migrate-strategies-to-feature-strategies.js @@ -1,16 +1,16 @@ const { v4: uuid } = require('uuid'); -exports.up = function(db, cb) { +exports.up = function (db, cb) { db.runSql( `SELECT * FROM features`, (err, results) => { - results.rows.forEach(feature => { + results.rows.forEach((feature) => { db.runSql( - `INSERT INTO feature_environments(feature_name, enabled) VALUES (?, ?)`, + 'INSERT INTO feature_environments(feature_name, enabled) VALUES (?, ?)', [feature.name, feature.enabled], ); - feature.strategies.forEach(strategy => { + feature.strategies.forEach((strategy) => { db.runSql( `INSERT INTO feature_strategies(id, feature_name, project_name, strategy_name, parameters, constraints) VALUES (?, ?, ?, ?, ?, ?)`, @@ -30,9 +30,9 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { +exports.down = function (db, cb) { db.runSql( - `DELETE FROM feature_strategies; DELETE FROM feature_environments;`, + 'DELETE FROM feature_strategies; DELETE FROM feature_environments;', cb, ); }; diff --git a/src/migrations/20210618091331-project-environments-table.js b/src/migrations/20210618091331-project-environments-table.js index 085629ac16..b74c1c3488 100644 --- a/src/migrations/20210618091331-project-environments-table.js +++ b/src/migrations/20210618091331-project-environments-table.js @@ -1,4 +1,4 @@ -exports.up = function(db, cb) { +exports.up = function (db, cb) { db.runSql( ` CREATE TABLE project_environments ( @@ -11,8 +11,8 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { - db.runSql(`DROP TABLE project_environments`, cb); +exports.down = function (db, cb) { + db.runSql('DROP TABLE project_environments', cb); }; exports._meta = { diff --git a/src/migrations/20210618100913-add-cascade-for-user-feedback.js b/src/migrations/20210618100913-add-cascade-for-user-feedback.js index a47ade3310..7200e2d40d 100644 --- a/src/migrations/20210618100913-add-cascade-for-user-feedback.js +++ b/src/migrations/20210618100913-add-cascade-for-user-feedback.js @@ -1,4 +1,4 @@ -exports.up = function(db, cb) { +exports.up = function (db, cb) { db.runSql( ` ALTER TABLE user_feedback DROP CONSTRAINT user_feedback_user_id_fkey; @@ -11,6 +11,6 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { - db.runSql(``, cb); +exports.down = function (db, cb) { + db.runSql('', cb); }; diff --git a/src/migrations/20210624114602-change-type-of-feature-archived.js b/src/migrations/20210624114602-change-type-of-feature-archived.js index f7075caae2..3dfa8faa74 100644 --- a/src/migrations/20210624114602-change-type-of-feature-archived.js +++ b/src/migrations/20210624114602-change-type-of-feature-archived.js @@ -1,6 +1,6 @@ 'use strict'; -exports.up = function(db, cb) { +exports.up = function (db, cb) { db.runSql( ` ALTER TABLE features ALTER COLUMN archived DROP DEFAULT; @@ -11,7 +11,7 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { +exports.down = function (db, cb) { db.runSql( ` ALTER TABLE features ALTER COLUMN archived DROP DEFAULT; diff --git a/src/migrations/20210624114855-drop-strategies-column-from-features.js b/src/migrations/20210624114855-drop-strategies-column-from-features.js index 6f0a5f1b05..63b39bbf4f 100644 --- a/src/migrations/20210624114855-drop-strategies-column-from-features.js +++ b/src/migrations/20210624114855-drop-strategies-column-from-features.js @@ -1,4 +1,4 @@ -exports.up = function(db, cb) { +exports.up = function (db, cb) { db.runSql( ` ALTER TABLE features @@ -8,7 +8,7 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { +exports.down = function (db, cb) { db.runSql( ` ALTER TABLE features diff --git a/src/migrations/20210624115109-drop-enabled-column-from-features.js b/src/migrations/20210624115109-drop-enabled-column-from-features.js index de64994329..feda91698c 100644 --- a/src/migrations/20210624115109-drop-enabled-column-from-features.js +++ b/src/migrations/20210624115109-drop-enabled-column-from-features.js @@ -1,4 +1,4 @@ -exports.up = function(db, cb) { +exports.up = function (db, cb) { db.runSql( ` ALTER TABLE features @@ -8,7 +8,7 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { +exports.down = function (db, cb) { db.runSql( ` ALTER TABLE features diff --git a/src/migrations/20210625102126-connect-default-project-to-global-environment.js b/src/migrations/20210625102126-connect-default-project-to-global-environment.js index 59fa9002d8..ac3fd824b0 100644 --- a/src/migrations/20210625102126-connect-default-project-to-global-environment.js +++ b/src/migrations/20210625102126-connect-default-project-to-global-environment.js @@ -1,4 +1,4 @@ -exports.up = function(db, cb) { +exports.up = function (db, cb) { db.runSql( ` INSERT INTO project_environments(project_id, environment_name) VALUES ('default', ':global:'); @@ -7,7 +7,7 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { +exports.down = function (db, cb) { db.runSql( ` DELETE FROM project_environments WHERE project_id = 'default' AND environment_name = ':global:'; diff --git a/src/migrations/20210629130734-add-health-rating-to-project.js b/src/migrations/20210629130734-add-health-rating-to-project.js index cc9a325c91..184a2255fa 100644 --- a/src/migrations/20210629130734-add-health-rating-to-project.js +++ b/src/migrations/20210629130734-add-health-rating-to-project.js @@ -1,4 +1,4 @@ -exports.up = function(db, cb) { +exports.up = function (db, cb) { db.runSql( ` ALTER TABLE projects ADD COLUMN health integer DEFAULT 100; @@ -7,7 +7,7 @@ exports.up = function(db, cb) { ); }; -exports.down = function(db, cb) { +exports.down = function (db, cb) { db.runSql( ` ALTER TABLE projects DROP COLUMN health; diff --git a/src/test/config/test-config.ts b/src/test/config/test-config.ts index 76b19fd641..6410b87452 100644 --- a/src/test/config/test-config.ts +++ b/src/test/config/test-config.ts @@ -9,7 +9,7 @@ import getLogger from '../fixtures/no-logger'; import { createConfig } from '../../lib/create-config'; function mergeAll(objects: Partial[]): T { - return merge.all(objects.filter(i => i)); + return merge.all(objects.filter((i) => i)); } export function createTestConfig(config?: IUnleashOptions): IUnleashConfig { diff --git a/src/test/e2e/api/admin/addon.e2e.test.js b/src/test/e2e/api/admin/addon.e2e.test.ts similarity index 89% rename from src/test/e2e/api/admin/addon.e2e.test.js rename to src/test/e2e/api/admin/addon.e2e.test.ts index d0c0155d64..ea3628ea8c 100644 --- a/src/test/e2e/api/admin/addon.e2e.test.js +++ b/src/test/e2e/api/admin/addon.e2e.test.ts @@ -1,8 +1,6 @@ -'use strict'; - -const dbInit = require('../../helpers/database-init'); -const { setupApp } = require('../../helpers/test-helper'); -const getLogger = require('../../../fixtures/no-logger'); +import dbInit from '../../helpers/database-init'; +import { setupApp } from '../../helpers/test-helper'; +import getLogger from '../../../fixtures/no-logger'; const MASKED_VALUE = '*****'; @@ -26,7 +24,7 @@ test('gets all addons', async () => { .get('/api/admin/addons') .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.addons.length).toBe(0); expect(res.body.providers.length).toBe(4); expect(res.body.providers[0].name).toBe('webhook'); @@ -54,10 +52,7 @@ test('should create addon configuration', async () => { events: ['feature-updated', 'feature-created'], }; - return app.request - .post('/api/admin/addons') - .send(config) - .expect(201); + return app.request.post('/api/admin/addons').send(config).expect(201); }); test('should delete addon configuration', async () => { @@ -119,7 +114,7 @@ test('should update addon configuration', async () => { .get(`/api/admin/addons/${id}`) .send(config) .expect(200) - .expect(r => { + .expect((r) => { expect(r.body.parameters.url).toBe(MASKED_VALUE); expect(r.body.parameters.bodyTemplate).toBe( updatedConfig.parameters.bodyTemplate, @@ -139,10 +134,7 @@ test('should not update with invalid addon configuration', async () => { events: ['feature-updated', 'feature-created'], }; - await app.request - .put('/api/admin/addons/1') - .send(config) - .expect(400); + await app.request.put('/api/admin/addons/1').send(config).expect(400); }); test('should not update unknown addon configuration', async () => { @@ -158,10 +150,7 @@ test('should not update unknown addon configuration', async () => { events: ['feature-updated', 'feature-created'], }; - await app.request - .put('/api/admin/addons/123123') - .send(config) - .expect(404); + await app.request.put('/api/admin/addons/123123').send(config).expect(404); }); test('should get addon configuration', async () => { @@ -187,7 +176,7 @@ test('should get addon configuration', async () => { await app.request .get(`/api/admin/addons/${id}`) .expect(200) - .expect(r => { + .expect((r) => { expect(r.body.provider).toBe(config.provider); expect(r.body.parameters.bodyTemplate).toBe( config.parameters.bodyTemplate, diff --git a/src/test/e2e/api/admin/api-token.auth.e2e.test.ts b/src/test/e2e/api/admin/api-token.auth.e2e.test.ts index 18fe721687..482784d9a4 100644 --- a/src/test/e2e/api/admin/api-token.auth.e2e.test.ts +++ b/src/test/e2e/api/admin/api-token.auth.e2e.test.ts @@ -1,8 +1,8 @@ import { setupAppWithCustomAuth } from '../../helpers/test-helper'; import dbInit from '../../helpers/database-init'; import getLogger from '../../../fixtures/no-logger'; -import { ApiTokenType } from '../../../../lib/db/api-token-store'; -import { RoleName } from '../../../../lib/services/access-service'; +import { ApiTokenType } from '../../../../lib/types/stores/api-token-store'; +import { RoleName } from '../../../../lib/types/model'; let stores; let db; @@ -57,7 +57,7 @@ test('none-admins should only get client tokens', async () => { .get('/api/admin/api-tokens') .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.tokens.length).toBe(1); expect(res.body.tokens[0].type).toBe(ApiTokenType.CLIENT); }); diff --git a/src/test/e2e/api/admin/api-token.e2e.test.ts b/src/test/e2e/api/admin/api-token.e2e.test.ts index f6e2862d09..a6f5b8db14 100644 --- a/src/test/e2e/api/admin/api-token.e2e.test.ts +++ b/src/test/e2e/api/admin/api-token.e2e.test.ts @@ -1,7 +1,7 @@ import { setupApp } from '../../helpers/test-helper'; import dbInit from '../../helpers/database-init'; import getLogger from '../../../fixtures/no-logger'; -import { ApiTokenType } from '../../../../lib/db/api-token-store'; +import { ApiTokenType } from '../../../../lib/types/stores/api-token-store'; let db; let app; @@ -28,7 +28,7 @@ test('returns empty list of tokens', async () => { .get('/api/admin/api-tokens') .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.tokens.length).toBe(0); }); }); @@ -43,7 +43,7 @@ test('creates new client token', async () => { }) .set('Content-Type', 'application/json') .expect(201) - .expect(res => { + .expect((res) => { expect(res.body.username).toBe('default-client'); expect(res.body.type).toBe('client'); expect(res.body.createdAt).toBeTruthy(); @@ -61,7 +61,7 @@ test('creates new admin token', async () => { }) .set('Content-Type', 'application/json') .expect(201) - .expect(res => { + .expect((res) => { expect(res.body.username).toBe('default-admin'); expect(res.body.type).toBe('admin'); expect(res.body.createdAt).toBeTruthy(); @@ -83,7 +83,7 @@ test('creates new admin token with expiry', async () => { }) .set('Content-Type', 'application/json') .expect(201) - .expect(res => { + .expect((res) => { expect(res.body.expiresAt).toBe(expiresAtAsISOStr); }); }); @@ -111,7 +111,7 @@ test('update admin token with expiry', async () => { .get('/api/admin/api-tokens') .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.tokens.length).toBe(1); expect(res.body.tokens[0].expiresAt).toBeTruthy(); }); @@ -140,7 +140,7 @@ test('creates a lot of client tokens', async () => { .get('/api/admin/api-tokens') .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.tokens.length).toBe(10); expect(res.body.tokens[2].type).toBe('client'); }); @@ -166,7 +166,7 @@ test('removes api token', async () => { .get('/api/admin/api-tokens') .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.tokens.length).toBe(0); }); }); diff --git a/src/test/e2e/api/admin/archive.test.ts b/src/test/e2e/api/admin/archive.test.ts index e40ee6f907..48d0b29d2d 100644 --- a/src/test/e2e/api/admin/archive.test.ts +++ b/src/test/e2e/api/admin/archive.test.ts @@ -20,7 +20,7 @@ test('Should get empty features via admin', async () => { .get('/api/admin/archive/features') .expect(200) .expect('Content-Type', /json/) - .expect(res => { + .expect((res) => { expect(res.body.features).toHaveLength(0); }); }); @@ -63,7 +63,7 @@ test('Should get archived toggles via admin', async () => { .get('/api/admin/archive/features') .expect(200) .expect('Content-Type', /json/) - .expect(res => { + .expect((res) => { expect(res.body.features).toHaveLength(2); }); }); diff --git a/src/test/e2e/api/admin/boostrap.test.ts b/src/test/e2e/api/admin/boostrap.test.ts new file mode 100644 index 0000000000..4a0c20e709 --- /dev/null +++ b/src/test/e2e/api/admin/boostrap.test.ts @@ -0,0 +1,43 @@ +import dbInit from '../../helpers/database-init'; +import getLogger from '../../../fixtures/no-logger'; +import { setupAppWithAuth } from '../../helpers/test-helper'; + +let app; +let db; + +const email = 'user@getunleash.io'; + +beforeAll(async () => { + db = await dbInit('user_api_serial', getLogger); + app = await setupAppWithAuth(db.stores); +}); + +afterAll(async () => { + await app.destroy(); + await db.destroy(); +}); + +test('Should get ui-bootstrap data', async () => { + // login + await app.request + .post('/api/admin/login') + .send({ + email, + }) + .expect(200); + + // get user data + await app.request + .get('/api/admin/ui-bootstrap') + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + const bootstrap = res.body; + expect(bootstrap.context).toBeDefined(); + expect(bootstrap.featureTypes).toBeDefined(); + expect(bootstrap.uiConfig).toBeDefined(); + expect(bootstrap.user).toBeDefined(); + expect(bootstrap.context.length).toBeGreaterThan(0); + expect(bootstrap.user.email).toBe(email); + }); +}); diff --git a/src/test/e2e/api/admin/context.e2e.test.js b/src/test/e2e/api/admin/context.e2e.test.ts similarity index 94% rename from src/test/e2e/api/admin/context.e2e.test.js rename to src/test/e2e/api/admin/context.e2e.test.ts index 906b9235f5..5d7d15c622 100644 --- a/src/test/e2e/api/admin/context.e2e.test.js +++ b/src/test/e2e/api/admin/context.e2e.test.ts @@ -1,8 +1,6 @@ -'use strict'; - -const dbInit = require('../../helpers/database-init'); -const { setupApp } = require('../../helpers/test-helper'); -const getLogger = require('../../../fixtures/no-logger'); +import dbInit from '../../helpers/database-init'; +import { setupApp } from '../../helpers/test-helper'; +import getLogger from '../../../fixtures/no-logger'; let db; let app; @@ -23,7 +21,7 @@ test('gets all context fields', async () => { .get('/api/admin/context') .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.length).toBe(3); }); }); @@ -34,7 +32,7 @@ test('get the context field', async () => { .get('/api/admin/context/environment') .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.name).toBe('environment'); }); }); diff --git a/src/test/e2e/api/admin/environment.test.ts b/src/test/e2e/api/admin/environment.test.ts index 3426ed831d..91d802ef06 100644 --- a/src/test/e2e/api/admin/environment.test.ts +++ b/src/test/e2e/api/admin/environment.test.ts @@ -46,7 +46,7 @@ test('Can list all existing environments', async () => { .get('/api/admin/environments') .expect(200) .expect('Content-Type', /json/) - .expect(res => { + .expect((res) => { expect(res.body.version).toBe(1); expect(res.body.environments[0]).toStrictEqual({ displayName: 'Across all environments', @@ -87,9 +87,11 @@ test('Can update environment', async () => { .put(`/api/admin/environments/${envName}`) .send({ displayName: 'Update this' }) .expect(200); - await app.request.get(`/api/admin/environments/${envName}`).expect(res => { - expect(res.body.displayName).toBe('Update this'); - }); + await app.request + .get(`/api/admin/environments/${envName}`) + .expect((res) => { + expect(res.body.displayName).toBe('Update this'); + }); }); test('Updating a non existing environment yields 404', async () => { diff --git a/src/test/e2e/api/admin/event.e2e.test.js b/src/test/e2e/api/admin/event.e2e.test.ts similarity index 76% rename from src/test/e2e/api/admin/event.e2e.test.js rename to src/test/e2e/api/admin/event.e2e.test.ts index eaddada78e..ad50354761 100644 --- a/src/test/e2e/api/admin/event.e2e.test.js +++ b/src/test/e2e/api/admin/event.e2e.test.ts @@ -1,8 +1,6 @@ -'use strict'; - -const { setupApp } = require('../../helpers/test-helper'); -const dbInit = require('../../helpers/database-init'); -const getLogger = require('../../../fixtures/no-logger'); +import { setupApp } from '../../helpers/test-helper'; +import dbInit from '../../helpers/database-init'; +import getLogger from '../../../fixtures/no-logger'; let app; let db; diff --git a/src/test/e2e/api/admin/feature-archive.e2e.test.js b/src/test/e2e/api/admin/feature-archive.e2e.test.ts similarity index 89% rename from src/test/e2e/api/admin/feature-archive.e2e.test.js rename to src/test/e2e/api/admin/feature-archive.e2e.test.ts index 60ba994563..bbbaf29b0a 100644 --- a/src/test/e2e/api/admin/feature-archive.e2e.test.js +++ b/src/test/e2e/api/admin/feature-archive.e2e.test.ts @@ -1,8 +1,6 @@ -'use strict'; - -const { setupApp } = require('../../helpers/test-helper'); -const dbInit = require('../../helpers/database-init'); -const getLogger = require('../../../fixtures/no-logger'); +import { setupApp } from '../../helpers/test-helper'; +import dbInit from '../../helpers/database-init'; +import getLogger from '../../../fixtures/no-logger'; let app; let db; @@ -95,7 +93,7 @@ test('returns three archived toggles', async () => { .get('/api/admin/archive/features') .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.features.length === 3).toBe(true); }); }); @@ -133,18 +131,18 @@ test('should be allowed to reuse deleted toggle name', async () => { }) .set('Content-Type', 'application/json') .expect(201) - .expect(res => { + .expect((res) => { expect(res.body.name).toBe('really.delete.feature'); expect(res.body.createdAt).toBeTruthy(); }); await app.request - .delete(`/api/admin/features/really.delete.feature`) + .delete('/api/admin/features/really.delete.feature') .expect(200); await app.request - .delete(`/api/admin/archive/really.delete.feature`) + .delete('/api/admin/archive/really.delete.feature') .expect(200); return app.request - .post(`/api/admin/features/validate`) + .post('/api/admin/features/validate') .send({ name: 'really.delete.feature' }) .set('Content-Type', 'application/json') .expect(200); @@ -160,15 +158,15 @@ test('Deleting an unarchived toggle should not take effect', async () => { }) .set('Content-Type', 'application/json') .expect(201) - .expect(res => { + .expect((res) => { expect(res.body.name).toBe('really.delete.feature'); expect(res.body.createdAt).toBeTruthy(); }); await app.request - .delete(`/api/admin/archive/really.delete.feature`) + .delete('/api/admin/archive/really.delete.feature') .expect(200); return app.request - .post(`/api/admin/features/validate`) + .post('/api/admin/features/validate') .send({ name: 'really.delete.feature' }) .set('Content-Type', 'application/json') .expect(409); // because it still exists diff --git a/src/test/e2e/api/admin/feature-type.test.ts b/src/test/e2e/api/admin/feature-type.test.ts new file mode 100644 index 0000000000..beebe404a3 --- /dev/null +++ b/src/test/e2e/api/admin/feature-type.test.ts @@ -0,0 +1,30 @@ +import dbInit from '../../helpers/database-init'; +import getLogger from '../../../fixtures/no-logger'; +import { setupApp } from '../../helpers/test-helper'; + +let app; +let db; + +beforeAll(async () => { + db = await dbInit('feature_type_api_serial', getLogger); + app = await setupApp(db.stores); +}); + +afterAll(async () => { + await app.destroy(); + await db.destroy(); +}); + +test('Should get all defined feature types', async () => { + await app.request + .get('/api/admin/feature-types') + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + const { version, types } = res.body; + + expect(version).toBe(1); + expect(types.length).toBe(5); + expect(types[0].name).toBe('Release'); + }); +}); diff --git a/src/test/e2e/api/admin/feature.auth.e2e.test.js b/src/test/e2e/api/admin/feature.auth.e2e.test.ts similarity index 83% rename from src/test/e2e/api/admin/feature.auth.e2e.test.js rename to src/test/e2e/api/admin/feature.auth.e2e.test.ts index 57c8fe97d9..de4a6b8a0d 100644 --- a/src/test/e2e/api/admin/feature.auth.e2e.test.js +++ b/src/test/e2e/api/admin/feature.auth.e2e.test.ts @@ -1,8 +1,6 @@ -'use strict'; - -const { setupAppWithAuth } = require('../../helpers/test-helper'); -const dbInit = require('../../helpers/database-init'); -const getLogger = require('../../../fixtures/no-logger'); +import { setupAppWithAuth } from '../../helpers/test-helper'; +import dbInit from '../../helpers/database-init'; +import getLogger from '../../../fixtures/no-logger'; let db; @@ -34,7 +32,7 @@ test('creates new feature toggle with createdBy', async () => { }) .expect(201); - await request.get('/api/admin/events/com.test.Username').expect(res => { + await request.get('/api/admin/events/com.test.Username').expect((res) => { expect(res.body.events[0].createdBy).toBe('user@mail.com'); }); diff --git a/src/test/e2e/api/admin/feature.custom-auth.e2e.test.js b/src/test/e2e/api/admin/feature.custom-auth.e2e.test.ts similarity index 83% rename from src/test/e2e/api/admin/feature.custom-auth.e2e.test.js rename to src/test/e2e/api/admin/feature.custom-auth.e2e.test.ts index 99158e6809..6b5803cdcb 100644 --- a/src/test/e2e/api/admin/feature.custom-auth.e2e.test.js +++ b/src/test/e2e/api/admin/feature.custom-auth.e2e.test.ts @@ -1,10 +1,8 @@ -'use strict'; +import { setupAppWithCustomAuth } from '../../helpers/test-helper'; +import AuthenticationRequired from '../../../../lib/types/authentication-required'; -const { setupAppWithCustomAuth } = require('../../helpers/test-helper'); -const AuthenticationRequired = require('../../../../lib/types/authentication-required'); - -const dbInit = require('../../helpers/database-init'); -const getLogger = require('../../../fixtures/no-logger'); +import dbInit from '../../helpers/database-init'; +import getLogger from '../../../fixtures/no-logger'; let stores; let db; @@ -22,7 +20,7 @@ afterAll(async () => { test('should require authenticated user', async () => { expect.assertions(0); - const preHook = app => { + const preHook = (app) => { app.use('/api/admin/', (req, res) => res .status('401') @@ -63,7 +61,7 @@ test('creates new feature toggle with createdBy', async () => { }) .expect(201); - await request.get('/api/admin/events/com.test.Username').expect(res => { + await request.get('/api/admin/events/com.test.Username').expect((res) => { expect(res.body.events[0].createdBy).toBe(email); }); diff --git a/src/test/e2e/api/admin/feature.e2e.test.js b/src/test/e2e/api/admin/feature.e2e.test.ts similarity index 94% rename from src/test/e2e/api/admin/feature.e2e.test.js rename to src/test/e2e/api/admin/feature.e2e.test.ts index e108427394..f6e5c3f23d 100644 --- a/src/test/e2e/api/admin/feature.e2e.test.js +++ b/src/test/e2e/api/admin/feature.e2e.test.ts @@ -1,9 +1,7 @@ -'use strict'; - -const faker = require('faker'); -const dbInit = require('../../helpers/database-init'); -const { setupApp } = require('../../helpers/test-helper'); -const getLogger = require('../../../fixtures/no-logger'); +import faker from 'faker'; +import dbInit from '../../helpers/database-init'; +import { setupApp } from '../../helpers/test-helper'; +import getLogger from '../../../fixtures/no-logger'; let app; let db; @@ -141,7 +139,7 @@ test('returns list of feature toggles', async () => .get('/api/admin/features') .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.features).toHaveLength(4); })); @@ -169,7 +167,7 @@ test('creates new feature toggle', async () => { }) .set('Content-Type', 'application/json') .expect(201) - .expect(res => { + .expect((res) => { expect(res.body.name).toBe('com.test.feature'); expect(res.body.createdAt).toBeTruthy(); }); @@ -197,7 +195,7 @@ test('fetch feature toggle with variants', async () => { return app.request .get('/api/admin/features/feature.with.variants') .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.variants).toHaveLength(2); }); }); @@ -212,7 +210,7 @@ test('creates new feature toggle with createdBy unknown', async () => { strategies: [{ name: 'default' }], }) .expect(201); - await app.request.get('/api/admin/events').expect(res => { + await app.request.get('/api/admin/events').expect((res) => { expect(res.body.events[0].createdBy).toBe('unknown'); }); }); @@ -258,13 +256,14 @@ test('can not toggle of feature that does not exist', async () => { test('can toggle a feature that does exist', async () => { expect.assertions(0); - const feature = await app.services.featureToggleServiceV2.createFeatureToggle( - 'default', - { - name: 'existing.feature', - }, - 'test', - ); + const feature = + await app.services.featureToggleServiceV2.createFeatureToggle( + 'default', + { + name: 'existing.feature', + }, + 'test', + ); return app.request .post(`/api/admin/features/${feature.name}/toggle`) .set('Content-Type', 'application/json') @@ -366,7 +365,7 @@ test('creates new feature toggle without type', async () => { }); return app.request .get('/api/admin/features/com.test.noType') - .expect(res => { + .expect((res) => { expect(res.body.type).toBe('release'); }); }); @@ -382,7 +381,7 @@ test('creates new feature toggle with type', async () => { return app.request .get('/api/admin/features/com.test.withType') .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.type).toBe('killswitch'); }); }); @@ -404,7 +403,7 @@ test('tags feature with new tag', async () => { .set('Content-Type', 'application/json'); return app.request .get('/api/admin/features/test.feature/tags') - .expect(res => { + .expect((res) => { expect(res.body.tags[0].value).toBe('TeamGreen'); }); }); @@ -429,7 +428,7 @@ test('tagging a feature with an already existing tag should be a noop', async () .get('/api/admin/features/test.feature/tags') .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.tags).toHaveLength(1); }); }); @@ -460,7 +459,7 @@ test('can untag feature', async () => { .get(`/api/admin/features/${feature1Name}/tags`) .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.tags).toHaveLength(0); }); }); @@ -490,7 +489,7 @@ test('Can get features tagged by tag', async () => { .get(`/api/admin/features?tag=${tag.type}:${tag.value}`) .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.features).toHaveLength(1); expect(res.body.features[0].name).toBe(feature1Name); }); @@ -527,12 +526,12 @@ test('Can query for multiple tags using OR', async () => { ) .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.features).toHaveLength(2); - expect(res.body.features.some(f => f.name === feature1Name)).toBe( + expect(res.body.features.some((f) => f.name === feature1Name)).toBe( true, ); - expect(res.body.features.some(f => f.name === feature2Name)).toBe( + expect(res.body.features.some((f) => f.name === feature2Name)).toBe( true, ); }); @@ -577,12 +576,12 @@ test('Querying with multiple filters ANDs the filters', async () => { .get(`/api/admin/features?tag=${tag.type}:${tag.value}`) .expect('Content-Type', /json/) .expect(200) - .expect(res => expect(res.body.features).toHaveLength(2)); + .expect((res) => expect(res.body.features).toHaveLength(2)); await app.request .get(`/api/admin/features?namePrefix=test&tag=${tag.type}:${tag.value}`) .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.features).toHaveLength(1); expect(res.body.features[0].name).toBe(feature1Name); }); @@ -606,7 +605,7 @@ test('Tagging a feature with a tag it already has should return 409', async () = .post(`/api/admin/features/${feature1Name}/tags`) .send(tag) .expect(409) - .expect(res => { + .expect((res) => { expect(res.body.details[0].message).toBe( `${feature1Name} already had the tag: [${tag.type}:${tag.value}]`, ); @@ -619,7 +618,7 @@ test('marks feature toggle as stale', async () => { .post('/api/admin/features/featureZ/stale/on') .set('Content-Type', 'application/json'); - return app.request.get('/api/admin/features/featureZ').expect(res => { + return app.request.get('/api/admin/features/featureZ').expect((res) => { expect(res.body.stale).toBe(true); }); }); diff --git a/src/test/e2e/api/admin/feedback.e2e.test.ts b/src/test/e2e/api/admin/feedback.e2e.test.ts index 79af385d42..dc9205ed89 100644 --- a/src/test/e2e/api/admin/feedback.e2e.test.ts +++ b/src/test/e2e/api/admin/feedback.e2e.test.ts @@ -1,3 +1,4 @@ +import { Application, NextFunction, Request, Response } from 'express'; import { setupAppWithCustomAuth } from '../../helpers/test-helper'; import dbInit from '../../helpers/database-init'; import getLogger from '../../../fixtures/no-logger'; @@ -15,14 +16,21 @@ beforeAll(async () => { const email = 'custom-user@mail.com'; const preHook = ( - app: any, + application: Application, config: IUnleashConfig, { userService }: IUnleashServices, ) => { - app.use('/api/admin/', async (req, res, next) => { - req.user = await userService.loginUserWithoutPassword(email, true); - next(); - }); + application.use( + '/api/admin/', + async (req: Request, res: Response, next: NextFunction) => { + // @ts-ignore + req.user = await userService.loginUserWithoutPassword( + email, + true, + ); + next(); + }, + ); }; app = await setupAppWithCustomAuth(stores, preHook); @@ -42,7 +50,7 @@ test('it creates feedback for user', async () => { .set('Content-Type', 'application/json') .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.feedbackId).toBe('pnps'); }); }); @@ -56,7 +64,7 @@ test('it gives 400 when feedback is not present', async () => { .set('Content-Type', 'application/json') .expect('Content-Type', /json/) .expect(400) - .expect(res => { + .expect((res) => { expect(res.body.error).toBeTruthy(); }); }); @@ -70,7 +78,7 @@ test('it updates feedback for user', async () => { .set('Content-Type', 'application/json') .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.neverShow).toBe(true); }); }); @@ -83,7 +91,7 @@ test('it retrieves feedback for user', async () => { .set('Content-Type', 'application/json') .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.feedback.length).toBe(1); expect(res.body.feedback[0].feedbackId).toBe('pnps'); }); diff --git a/src/test/e2e/api/admin/metrics.e2e.test.js b/src/test/e2e/api/admin/metrics.e2e.test.ts similarity index 55% rename from src/test/e2e/api/admin/metrics.e2e.test.js rename to src/test/e2e/api/admin/metrics.e2e.test.ts index 003a0379ef..3f39dbcfb9 100644 --- a/src/test/e2e/api/admin/metrics.e2e.test.js +++ b/src/test/e2e/api/admin/metrics.e2e.test.ts @@ -1,8 +1,6 @@ -'use strict'; - -const dbInit = require('../../helpers/database-init'); -const { setupApp } = require('../../helpers/test-helper'); -const getLogger = require('../../../fixtures/no-logger'); +import dbInit from '../../helpers/database-init'; +import { setupApp } from '../../helpers/test-helper'; +import getLogger from '../../../fixtures/no-logger'; let app; let db; @@ -51,6 +49,24 @@ beforeEach(async () => { started: 1516026938494, interval: 10, }); + await app.services.clientMetricsService.addPayload({ + appName: 'demo-app-1', + instanceId: '123', + bucket: { + start: Date.now(), + stop: Date.now(), + toggles: { + someToggle: { + yes: 100, + no: 0, + }, + anotherToggle: { + yes: 0, + no: 1, + }, + }, + }, + }); }); afterAll(async () => { @@ -69,7 +85,7 @@ test('should get application details', async () => { .get('/api/admin/metrics/applications/demo-app-1') .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.appName).toBe('demo-app-1'); expect(res.body.instances).toHaveLength(1); }); @@ -81,22 +97,67 @@ test('should get list of applications', async () => { .get('/api/admin/metrics/applications') .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.applications).toHaveLength(3); }); }); +test('should get list of seen seen-apps', async () => { + return app.request + .get('/api/admin/metrics/seen-apps') + .expect('Content-Type', /json/) + .expect(200) + .expect((res) => { + expect(res.body.someToggle).toBeDefined(); + }); +}); + +test('should get list of seen seen-toggles', async () => { + return app.request + .get('/api/admin/metrics/seen-toggles') + .expect('Content-Type', /json/) + .expect(200) + .expect((res) => { + expect(res.body).toHaveLength(1); + expect(res.body[0].seenToggles).toContain('someToggle'); + }); +}); + +test('should get list of feature-toggle metrics', async () => { + return app.request + .get('/api/admin/metrics/feature-toggles') + .expect('Content-Type', /json/) + .expect(200) + .expect((res) => { + expect(res.body.lastHour).toBeDefined(); + expect(res.body.lastHour.anotherToggle).toBeDefined(); + expect(res.body.lastMinute).toBeDefined(); + expect(res.body.lastMinute.anotherToggle).toBeDefined(); + }); +}); + +test('should get feature-toggle metrics', async () => { + return app.request + .get('/api/admin/metrics/feature-toggles/anotherToggle') + .expect('Content-Type', /json/) + .expect(200) + .expect((res) => { + expect(res.body.lastHour).toBeDefined(); + expect(res.body.lastMinute).toBeDefined(); + }); +}); + test('should delete application', async () => { expect.assertions(2); await app.request .delete('/api/admin/metrics/applications/deletable-app') - .expect(res => { + .expect((res) => { expect(res.status).toBe(200); }); return app.request .get('/api/admin/metrics/applications') .expect('Content-Type', /json/) - .expect(res => { + .expect((res) => { expect(res.body.applications).toHaveLength(2); }); }); @@ -105,7 +166,7 @@ test('deleting an application should be idempotent, so expect 200', async () => expect.assertions(1); return app.request .delete('/api/admin/metrics/applications/unknown') - .expect(res => { + .expect((res) => { expect(res.status).toBe(200); }); }); diff --git a/src/test/e2e/api/admin/project/feature.strategy.e2e.test.ts b/src/test/e2e/api/admin/project/feature.strategy.e2e.test.ts index d313c39a64..df13b4d456 100644 --- a/src/test/e2e/api/admin/project/feature.strategy.e2e.test.ts +++ b/src/test/e2e/api/admin/project/feature.strategy.e2e.test.ts @@ -25,7 +25,7 @@ test('Trying to add a strategy configuration to environment not connected to tog }) .set('Content-Type', 'application/json') .expect(201) - .expect(res => { + .expect((res) => { expect(res.body.name).toBe('com.test.feature'); expect(res.body.createdAt).toBeTruthy(); }); @@ -40,7 +40,7 @@ test('Trying to add a strategy configuration to environment not connected to tog }, }) .expect(400) - .expect(r => { + .expect((r) => { expect(r.body.details[0].message).toBe( 'You have not added the current environment to the project', ); @@ -57,14 +57,14 @@ test('Can get project overview', async () => { }) .set('Content-Type', 'application/json') .expect(201) - .expect(res => { + .expect((res) => { expect(res.body.name).toBe('project-overview'); expect(res.body.createdAt).toBeTruthy(); }); await app.request .get('/api/admin/projects/default') .expect(200) - .expect(r => { + .expect((r) => { expect(r.body.name).toBe('Default'); expect(r.body.features).toHaveLength(2); expect(r.body.members).toBe(0); @@ -79,18 +79,18 @@ test('Can get features for project', async () => { }) .set('Content-Type', 'application/json') .expect(201) - .expect(res => { + .expect((res) => { expect(res.body.name).toBe('features-for-project'); expect(res.body.createdAt).toBeTruthy(); }); await app.request .get('/api/admin/projects/default/features') .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.version).toBeTruthy(); expect( res.body.features.some( - feature => feature.name === 'features-for-project', + (feature) => feature.name === 'features-for-project', ), ).toBeTruthy(); }); @@ -106,7 +106,7 @@ test('Project overview includes environment connected to feature', async () => { }) .set('Content-Type', 'application/json') .expect(201) - .expect(res => { + .expect((res) => { expect(res.body.name).toBe('com.test.environment'); expect(res.body.createdAt).toBeTruthy(); }); @@ -122,7 +122,7 @@ test('Project overview includes environment connected to feature', async () => { return app.request .get('/api/admin/projects/default') .expect(200) - .expect(r => { + .expect((r) => { expect(r.body.features[0].environments[0].name).toBe(':global:'); expect(r.body.features[0].environments[1].name).toBe( 'project-overview', @@ -140,7 +140,7 @@ test('Disconnecting environment from project, removes environment from features }) .set('Content-Type', 'application/json') .expect(201) - .expect(res => { + .expect((res) => { expect(res.body.name).toBe('com.test.disconnect.environment'); expect(res.body.createdAt).toBeTruthy(); }); @@ -159,10 +159,10 @@ test('Disconnecting environment from project, removes environment from features return app.request .get('/api/admin/projects/default') .expect(200) - .expect(r => { + .expect((r) => { expect( r.body.features.some( - e => e.environment === 'dis-project-overview', + (e) => e.environment === 'dis-project-overview', ), ).toBeFalsy(); }); @@ -196,13 +196,13 @@ test('Can enable/disable environment for feature', async () => { }) .set('Content-Type', 'application/json') .expect(201) - .expect(res => { + .expect((res) => { expect(res.body.name).toBe('com.test.enable.environment'); expect(res.body.createdAt).toBeTruthy(); }); await app.request .post( - `/api/admin/projects/default/features/com.test.enable.environment/environments/enable-feature-environment/on`, + '/api/admin/projects/default/features/com.test.enable.environment/environments/enable-feature-environment/on', ) .send({}) .expect(200); @@ -210,16 +210,16 @@ test('Can enable/disable environment for feature', async () => { .get('/api/admin/projects/default/features/com.test.enable.environment') .expect(200) .expect('Content-Type', /json/) - .expect(res => { + .expect((res) => { const enabledFeatureEnv = res.body.environments.find( - e => e.name === 'enable-feature-environment', + (e) => e.name === 'enable-feature-environment', ); expect(enabledFeatureEnv).toBeTruthy(); expect(enabledFeatureEnv.enabled).toBe(true); }); await app.request .post( - `/api/admin/projects/default/features/com.test.enable.environment/environments/enable-feature-environment/off`, + '/api/admin/projects/default/features/com.test.enable.environment/environments/enable-feature-environment/off', ) .send({}) .expect(200); @@ -227,16 +227,16 @@ test('Can enable/disable environment for feature', async () => { .get('/api/admin/projects/default/features/com.test.enable.environment') .expect(200) .expect('Content-Type', /json/) - .expect(res => { + .expect((res) => { const disabledFeatureEnv = res.body.environments.find( - e => e.name === 'enable-feature-environment', + (e) => e.name === 'enable-feature-environment', ); expect(disabledFeatureEnv).toBeTruthy(); expect(disabledFeatureEnv.enabled).toBe(false); }); }); -test(`Trying to get a project that doesn't exist yields 404`, async () => { +test("Trying to get a project that doesn't exist yields 404", async () => { await app.request.get('/api/admin/projects/nonexisting').expect(404); }); @@ -244,7 +244,7 @@ test('Trying to get features for non-existing project also yields 404', async () await app.request .get('/api/admin/projects/nonexisting/features') .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.features).toHaveLength(0); }); }); @@ -256,7 +256,7 @@ test('Can use new project feature toggle endpoint to create feature toggle witho name: 'new.toggle.without.strategy', }) .expect(201) - .expect(res => { + .expect((res) => { expect(res.body.project).toBe('default'); }); }); @@ -277,7 +277,7 @@ test('Trying to create toggle that already exists yield 409 error', async () => name: 'already.exists.test', }) .expect(201) - .expect(res => { + .expect((res) => { expect(res.body.project).toBe('default'); }); await app.request @@ -290,7 +290,7 @@ test('Trying to create toggle that already exists yield 409 error', async () => test('Trying to create toggle under project that does not exist should fail', async () => { await app.request - .post('/api/admin/projects/non-existing/features') + .post('/api/admin/projects/non-existing-secondary/features') .send({ name: 'project.does.not.exist', }) @@ -325,7 +325,7 @@ test('Can get environment info for feature toggle', async () => { `/api/admin/projects/default/features/environment.info/environments/${envName}`, ) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.enabled).toBe(false); expect(res.body.environment).toBe(envName); expect(res.body.strategies).toHaveLength(0); @@ -339,7 +339,7 @@ test('Getting environment info for environment that does not exist yields 404', .expect(201); await app.request .get( - `/api/admin/projects/default/features/non.existing.env/environments/non.existing.environment`, + '/api/admin/projects/default/features/non.existing.env/environments/non.existing.environment', ) .expect(404); }); @@ -351,13 +351,13 @@ test('Trying to toggle environment that does not exist yields 404', async () => .expect(201); await app.request .post( - `/api/admin/projects/default/features/toggle.env/environments/does-not-exist/on`, + '/api/admin/projects/default/features/toggle.env/environments/does-not-exist/on', ) .send({}) .expect(404); await app.request .post( - `/api/admin/projects/default/features/toggle.env/environments/does-not-exist/off`, + '/api/admin/projects/default/features/toggle.env/environments/does-not-exist/off', ) .send({}) .expect(404); @@ -365,7 +365,7 @@ test('Trying to toggle environment that does not exist yields 404', async () => test('Getting feature that does not exist should yield 404', async () => { await app.request - .get(`/api/admin/projects/default/features/non.existing.feature`) + .get('/api/admin/projects/default/features/non.existing.feature') .expect(404); }); @@ -405,7 +405,7 @@ test('Can add strategy to feature toggle', async () => { .expect(200); await app.request .get(`/api/admin/projects/default/features/${featureName}`) - .expect(res => { + .expect((res) => { expect(res.body.environments[0].strategies).toHaveLength(1); }); }); @@ -449,7 +449,7 @@ test('Can get strategies for feature and environment', async () => { `/api/admin/projects/default/features/${featureName}/environments/${envName}/strategies`, ) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body).toHaveLength(1); expect(res.body[0].parameters.userId).toBe('string'); }); @@ -503,7 +503,7 @@ test('Can update a strategy based on id', async () => { }, }) .expect(200) - .expect(res => { + .expect((res) => { strategy = res.body; }); @@ -518,7 +518,7 @@ test('Can update a strategy based on id', async () => { `/api/admin/projects/default/features/${featureName}/environments/${envName}/strategies/${strategy.id}`, ) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.parameters.companyId).toBeTruthy(); expect(res.body.parameters.userId).toBeTruthy(); }); diff --git a/src/test/e2e/api/admin/project/project.health.e2e.test.ts b/src/test/e2e/api/admin/project/project.health.e2e.test.ts index 808a1aa783..7c3d3eea22 100644 --- a/src/test/e2e/api/admin/project/project.health.e2e.test.ts +++ b/src/test/e2e/api/admin/project/project.health.e2e.test.ts @@ -47,7 +47,7 @@ test('Project with no stale toggles should have 100% health rating', async () => .get('/api/admin/projects/fresh') .expect(200) .expect('Content-Type', /json/) - .expect(res => { + .expect((res) => { expect(res.body.health).toBe(100); }); }); @@ -88,7 +88,7 @@ test('Health rating endpoint yields stale, potentially stale and active count on .get(`/api/admin/projects/${project.id}/health-report`) .expect(200) .expect('Content-Type', /json/) - .expect(res => { + .expect((res) => { expect(res.body.health).toBe(67); expect(res.body.activeCount).toBe(2); expect(res.body.staleCount).toBe(1); @@ -140,7 +140,7 @@ test('Health rating endpoint correctly handles potentially stale toggles', async .get(`/api/admin/projects/${project.id}/health-report`) .expect(200) .expect('Content-Type', /json/) - .expect(res => { + .expect((res) => { expect(res.body.health).toBe(50); expect(res.body.activeCount).toBe(3); expect(res.body.staleCount).toBe(1); diff --git a/src/test/e2e/api/admin/state.e2e.test.js b/src/test/e2e/api/admin/state.e2e.test.ts similarity index 91% rename from src/test/e2e/api/admin/state.e2e.test.js rename to src/test/e2e/api/admin/state.e2e.test.ts index 000a197027..a05be8046d 100644 --- a/src/test/e2e/api/admin/state.e2e.test.js +++ b/src/test/e2e/api/admin/state.e2e.test.ts @@ -1,9 +1,8 @@ -'use strict'; +import dbInit from '../../helpers/database-init'; +import { setupApp } from '../../helpers/test-helper'; +import getLogger from '../../../fixtures/no-logger'; const importData = require('../../../examples/import.json'); -const dbInit = require('../../helpers/database-init'); -const { setupApp } = require('../../helpers/test-helper'); -const getLogger = require('../../../fixtures/no-logger'); let app; let db; @@ -25,7 +24,7 @@ test('exports strategies and features as json by default', async () => { .get('/api/admin/state/export') .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect('features' in res.body).toBe(true); expect('strategies' in res.body).toBe(true); }); diff --git a/src/test/e2e/api/admin/strategy.e2e.test.js b/src/test/e2e/api/admin/strategy.e2e.test.ts similarity index 87% rename from src/test/e2e/api/admin/strategy.e2e.test.js rename to src/test/e2e/api/admin/strategy.e2e.test.ts index 49567d8862..af1ee8d1a4 100644 --- a/src/test/e2e/api/admin/strategy.e2e.test.js +++ b/src/test/e2e/api/admin/strategy.e2e.test.ts @@ -1,8 +1,6 @@ -'use strict'; - -const dbInit = require('../../helpers/database-init'); -const { setupApp } = require('../../helpers/test-helper'); -const getLogger = require('../../../fixtures/no-logger'); +import dbInit from '../../helpers/database-init'; +import { setupApp } from '../../helpers/test-helper'; +import getLogger from '../../../fixtures/no-logger'; let app; let db; @@ -24,8 +22,8 @@ test('gets all strategies', async () => { .get('/api/admin/strategies') .expect('Content-Type', /json/) .expect(200) - .expect(res => { - expect(res.body.strategies.length === 2).toBe(true); + .expect((res) => { + expect(res.body.strategies).toHaveLength(2); }); }); @@ -135,7 +133,7 @@ test('deprecating a strategy works', async () => { .get(`/api/admin/strategies/${name}`) .expect('Content-Type', /json/) .expect(200) - .expect(res => expect(res.body.deprecated).toBe(true)); + .expect((res) => expect(res.body.deprecated).toBe(true)); }); test('can reactivate a deprecated strategy', async () => { @@ -154,7 +152,7 @@ test('can reactivate a deprecated strategy', async () => { .get(`/api/admin/strategies/${name}`) .expect('Content-Type', /json/) .expect(200) - .expect(res => expect(res.body.deprecated).toBe(true)); + .expect((res) => expect(res.body.deprecated).toBe(true)); await app.request .post(`/api/admin/strategies/${name}/reactivate`) .set('Content-Type', 'application/json') @@ -164,7 +162,7 @@ test('can reactivate a deprecated strategy', async () => { .get(`/api/admin/strategies/${name}`) .expect('Content-Type', /json/) .expect(200) - .expect(res => expect(res.body.deprecated).toBe(false)); + .expect((res) => expect(res.body.deprecated).toBe(false)); }); test('cannot deprecate default strategy', async () => { @@ -177,26 +175,25 @@ test('cannot deprecate default strategy', async () => { }); test('can update a exiting strategy with deprecated', async () => { - expect.assertions(0); - await app.request .post('/api/admin/strategies') .send({ - name: 'myCustomStrategyDepreacted', + name: 'myCustomStrategyDeprecated', description: 'Best strategy ever.', parameters: [], deprecated: true, }) - .set('Content-Type', 'application/json'); + .set('Content-Type', 'application/json') + .expect(201); const { body: strategy } = await app.request.get( - '/api/admin/strategies/myCustomStrategyDepreacted', + '/api/admin/strategies/myCustomStrategyDeprecated', ); strategy.description = 'A new desc'; return app.request - .put('/api/admin/strategies/default') + .put('/api/admin/strategies/myCustomStrategyDeprecated') .send(strategy) .set('Content-Type', 'application/json') .expect(200); diff --git a/src/test/e2e/api/admin/tag-types.e2e.test.js b/src/test/e2e/api/admin/tag-types.e2e.test.ts similarity index 90% rename from src/test/e2e/api/admin/tag-types.e2e.test.js rename to src/test/e2e/api/admin/tag-types.e2e.test.ts index 0d046da630..5079a39954 100644 --- a/src/test/e2e/api/admin/tag-types.e2e.test.js +++ b/src/test/e2e/api/admin/tag-types.e2e.test.ts @@ -1,8 +1,6 @@ -'use strict'; - -const dbInit = require('../../helpers/database-init'); -const { setupApp } = require('../../helpers/test-helper'); -const getLogger = require('../../../fixtures/no-logger'); +import dbInit from '../../helpers/database-init'; +import { setupApp } from '../../helpers/test-helper'; +import getLogger from '../../../fixtures/no-logger'; let app; let db; @@ -24,7 +22,7 @@ test('returns list of tag-types', async () => { .get('/api/admin/tag-types') .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.tagTypes.length).toBe(1); }); }); @@ -36,7 +34,7 @@ test('gets a tag-type by name', async () => { .get('/api/admin/tag-types/simple') .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.tagType.name).toBe('simple'); }); }); @@ -52,14 +50,13 @@ test('Can create a new tag type', async () => { name: 'slack', description: 'Tag your feature toggles with slack channel to post updates for toggle to', - icon: - 'http://icons.iconarchive.com/icons/papirus-team/papirus-apps/32/slack-icon.png', + icon: 'http://icons.iconarchive.com/icons/papirus-team/papirus-apps/32/slack-icon.png', }); return app.request .get('/api/admin/tag-types/slack') .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.tagType.icon).toBe( 'http://icons.iconarchive.com/icons/papirus-team/papirus-apps/32/slack-icon.png', ); @@ -76,7 +73,7 @@ test('Invalid tag types gets rejected', async () => { }) .set('Content-Type', 'application/json') .expect(400) - .expect(res => { + .expect((res) => { expect(res.body.details[0].message).toBe( '"name" must be URL friendly', ); @@ -96,7 +93,7 @@ test('Can update a tag types description and icon', async () => { .get('/api/admin/tag-types/simple') .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.tagType.icon).toBe('$'); }); }); @@ -109,7 +106,7 @@ test('Invalid updates gets rejected', async () => { icon: 125, }) .expect(400) - .expect(res => { + .expect((res) => { expect(res.body.details[0].message).toBe( '"description" must be a string', ); @@ -127,7 +124,7 @@ test('Validation of tag-types returns 200 for valid tag-types', async () => { }) .set('Content-Type', 'application/json') .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.valid).toBe(true); }); }); @@ -141,7 +138,7 @@ test('Invalid tag-types get refused by validator', async () => { }) .set('Content-Type', 'application/json') .expect(400) - .expect(res => { + .expect((res) => { expect(res.body.details[0].message).toBe( '"name" must be URL friendly', ); @@ -176,7 +173,7 @@ test('Non unique tag-types gets rejected', async () => { icon: 'T', }) .set('Content-Type', 'application/json') - .expect(res => { + .expect((res) => { expect(res.status).toBe(409); }); }); @@ -188,7 +185,7 @@ test('Only required argument should be name', async () => { .send({ name, description: '' }) .set('Content-Type', 'application/json') .expect(201) - .expect(res => { + .expect((res) => { expect(res.body.name).toBe(name); }); }); diff --git a/src/test/e2e/api/admin/tags.e2e.test.js b/src/test/e2e/api/admin/tags.e2e.test.ts similarity index 84% rename from src/test/e2e/api/admin/tags.e2e.test.js rename to src/test/e2e/api/admin/tags.e2e.test.ts index e6a5759114..c366cf6a39 100644 --- a/src/test/e2e/api/admin/tags.e2e.test.js +++ b/src/test/e2e/api/admin/tags.e2e.test.ts @@ -1,8 +1,6 @@ -'use strict'; - -const dbInit = require('../../helpers/database-init'); -const { setupApp } = require('../../helpers/test-helper'); -const getLogger = require('../../../fixtures/no-logger'); +import dbInit from '../../helpers/database-init'; +import { setupApp } from '../../helpers/test-helper'; +import getLogger from '../../../fixtures/no-logger'; let app; let db; @@ -30,7 +28,7 @@ test('returns list of tags', async () => { .get('/api/admin/tags') .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.tags.length).toBe(1); }); }); @@ -47,7 +45,7 @@ test('gets a tag by type and value', async () => { .get('/api/admin/tags/simple/Tester') .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.tag.value).toBe('Tester'); }); }); @@ -55,7 +53,7 @@ test('gets a tag by type and value', async () => { test('cannot get tag that does not exist', async () => { expect.assertions(1); - return app.request.get('/api/admin/tags/simple/12158091').expect(res => { + return app.request.get('/api/admin/tags/simple/12158091').expect((res) => { expect(res.status).toBe(404); }); }); @@ -68,7 +66,7 @@ test('Can create a tag', async () => value: 'TeamRed', type: 'simple', }) - .expect(res => { + .expect((res) => { expect(res.status).toBe(201); })); test('Can validate a tag', async () => @@ -80,7 +78,7 @@ test('Can validate a tag', async () => }) .expect('Content-Type', /json/) .expect(400) - .expect(res => { + .expect((res) => { expect(res.body.details.length).toBe(2); expect(res.body.details[0].message).toBe( '"value" must be a string', @@ -94,15 +92,15 @@ test('Can delete a tag', async () => { .delete('/api/admin/tags/simple/Tester') .set('Content-Type', 'application/json') .expect(200); - await new Promise(r => setTimeout(r, 50)); + await new Promise((r) => setTimeout(r, 50)); return app.request .get('/api/admin/tags') .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect( res.body.tags.indexOf( - tag => tag.value === 'Tester' && tag.type === 'simple', + (tag) => tag.value === 'Tester' && tag.type === 'simple', ), ).toBe(-1); }); diff --git a/src/test/e2e/api/admin/user-admin.e2e.test.ts b/src/test/e2e/api/admin/user-admin.e2e.test.ts index 9188a5b0d6..6c07415668 100644 --- a/src/test/e2e/api/admin/user-admin.e2e.test.ts +++ b/src/test/e2e/api/admin/user-admin.e2e.test.ts @@ -1,23 +1,23 @@ import { setupApp } from '../../helpers/test-helper'; import dbInit from '../../helpers/database-init'; import getLogger from '../../../fixtures/no-logger'; -import UserStore from '../../../../lib/db/user-store'; -import { AccessStore, IRole } from '../../../../lib/db/access-store'; -import { RoleName } from '../../../../lib/services/access-service'; -import EventStore from '../../../../lib/db/event-store'; import { USER_CREATED, USER_DELETED, USER_UPDATED, } from '../../../../lib/types/events'; +import { IAccessStore, IRole } from '../../../../lib/types/stores/access-store'; +import { IEventStore } from '../../../../lib/types/stores/event-store'; +import { IUserStore } from '../../../../lib/types/stores/user-store'; +import { RoleName } from '../../../../lib/types/model'; let stores; let db; let app; -let userStore: UserStore; -let eventStore: EventStore; -let accessStore: AccessStore; +let userStore: IUserStore; +let eventStore: IEventStore; +let accessStore: IAccessStore; let editorRole: IRole; let adminRole: IRole; @@ -30,8 +30,8 @@ beforeAll(async () => { accessStore = stores.accessStore; eventStore = stores.eventStore; const roles = await accessStore.getRootRoles(); - editorRole = roles.find(r => r.name === RoleName.EDITOR); - adminRole = roles.find(r => r.name === RoleName.ADMIN); + editorRole = roles.find((r) => r.name === RoleName.EDITOR); + adminRole = roles.find((r) => r.name === RoleName.ADMIN); }); afterAll(async () => { @@ -50,7 +50,7 @@ test('returns empty list of users', async () => { .get('/api/admin/user-admin') .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.users.length).toBe(0); }); }); @@ -58,7 +58,7 @@ test('returns empty list of users', async () => { test('creates and returns all users', async () => { expect.assertions(2); - const createUserRequests = [...Array(20).keys()].map(i => + const createUserRequests = [...Array(20).keys()].map((i) => app.request .post('/api/admin/user-admin') .send({ @@ -75,7 +75,7 @@ test('creates and returns all users', async () => { .get('/api/admin/user-admin') .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.users.length).toBe(20); expect(res.body.users[2].rootRole).toBe(editorRole.id); }); @@ -93,7 +93,7 @@ test('creates editor-user without password', async () => { }) .set('Content-Type', 'application/json') .expect(201) - .expect(res => { + .expect((res) => { expect(res.body.email).toBe('some@getunelash.ai'); expect(res.body.rootRole).toBe(editorRole.id); expect(res.body.id).toBeTruthy(); @@ -116,7 +116,7 @@ test('creates admin-user with password', async () => { expect(body.rootRole).toBe(adminRole.id); - const user = await userStore.get({ id: body.id }); + const user = await userStore.getByQuery({ id: body.id }); expect(user.email).toBe('some@getunelash.ai'); expect(user.name).toBe('Some Name'); @@ -161,7 +161,7 @@ test('update user name', async () => { }) .set('Content-Type', 'application/json') .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.email).toBe('some@getunelash.ai'); expect(res.body.name).toBe('New name'); expect(res.body.id).toBe(body.id); @@ -215,9 +215,9 @@ test('should search for users', async () => { return app.request .get('/api/admin/user-admin/search?q=another') .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.length).toBe(2); - expect(res.body.some(u => u.email === 'another@mail.com')).toBe( + expect(res.body.some((u) => u.email === 'another@mail.com')).toBe( true, ); }); @@ -235,7 +235,7 @@ test('Creates a user and includes inviteLink and emailConfigured', async () => { }) .set('Content-Type', 'application/json') .expect(201) - .expect(res => { + .expect((res) => { expect(res.body.email).toBe('some@getunelash.ai'); expect(res.body.rootRole).toBe(editorRole.id); expect(res.body.inviteLink).toBeTruthy(); diff --git a/src/test/e2e/api/admin/user.test.ts b/src/test/e2e/api/admin/user.test.ts new file mode 100644 index 0000000000..c93952c6a4 --- /dev/null +++ b/src/test/e2e/api/admin/user.test.ts @@ -0,0 +1,38 @@ +import dbInit from '../../helpers/database-init'; +import getLogger from '../../../fixtures/no-logger'; +import { setupAppWithAuth } from '../../helpers/test-helper'; + +let app; +let db; + +const email = 'user@getunleash.io'; + +beforeAll(async () => { + db = await dbInit('user_api_serial', getLogger); + app = await setupAppWithAuth(db.stores); +}); + +afterAll(async () => { + await app.destroy(); + await db.destroy(); +}); + +test('Should get my user data', async () => { + // login + await app.request + .post('/api/admin/login') + .send({ + email, + }) + .expect(200); + + // get user data + await app.request + .get('/api/admin/user') + .expect(200) + .expect('Content-Type', /json/) + .expect((res) => { + expect(res.body.user.email).toBe(email); + expect(res.body.permissions).toBeDefined(); + }); +}); diff --git a/src/test/e2e/api/auth/reset-password-controller.e2e.test.ts b/src/test/e2e/api/auth/reset-password-controller.e2e.test.ts index fc396ec376..e244c06d80 100644 --- a/src/test/e2e/api/auth/reset-password-controller.e2e.test.ts +++ b/src/test/e2e/api/auth/reset-password-controller.e2e.test.ts @@ -1,21 +1,18 @@ import { URL } from 'url'; import EventEmitter from 'events'; +import { createTestConfig } from '../../../config/test-config'; +import { IUnleashConfig } from '../../../../lib/types/option'; +import UserService from '../../../../lib/services/user-service'; +import { AccessService } from '../../../../lib/services/access-service'; +import ResetTokenService from '../../../../lib/services/reset-token-service'; +import { IUser } from '../../../../lib/types/user'; +import { setupApp, setupAppWithAuth } from '../../helpers/test-helper'; import dbInit from '../../helpers/database-init'; import getLogger from '../../../fixtures/no-logger'; - -import { - AccessService, - RoleName, -} from '../../../../lib/services/access-service'; -import ResetTokenService from '../../../../lib/services/reset-token-service'; -import UserService from '../../../../lib/services/user-service'; -import { setupApp, setupAppWithAuth } from '../../helpers/test-helper'; import { EmailService } from '../../../../lib/services/email-service'; -import User from '../../../../lib/types/user'; -import { IUnleashConfig } from '../../../../lib/types/option'; -import { createTestConfig } from '../../../config/test-config'; import SessionStore from '../../../../lib/db/session-store'; import SessionService from '../../../../lib/services/session-service'; +import { RoleName } from '../../../../lib/types/model'; let app; let stores; @@ -34,8 +31,8 @@ const password = 'DtUYwi&l5I1KX4@Le'; let userService: UserService; let accessService: AccessService; let resetTokenService: ResetTokenService; -let adminUser: User; -let user: User; +let adminUser: IUser; +let user: IUser; const getBackendResetUrl = (url: URL): string => { const urlString = url.toString(); @@ -96,7 +93,7 @@ test('Can validate token for password reset', async () => { .get(relative) .expect(200) .expect('Content-Type', /json/) - .expect(res => { + .expect((res) => { expect(res.body.email).toBe(user.email); }); }); @@ -117,7 +114,7 @@ test('Can use token to reset password', async () => { .get(relative) .expect(200) .expect('Content-Type', /json/) - .expect(res => { + .expect((res) => { token = res.body.token; }); await app.request @@ -142,7 +139,7 @@ test('Trying to reset password with same token twice does not work', async () => .get(relative) .expect(200) .expect('Content-Type', /json/) - .expect(res => { + .expect((res) => { token = res.body.token; }); await app.request @@ -161,13 +158,13 @@ test('Trying to reset password with same token twice does not work', async () => password, }) .expect(403) - .expect(res => { + .expect((res) => { expect(res.body.details[0].message).toBeTruthy(); }); }); test('Invalid token should yield 401', async () => - app.request.get('/auth/reset/validate?token=abc123').expect(res => { + app.request.get('/auth/reset/validate?token=abc123').expect((res) => { expect(res.status).toBe(401); })); @@ -187,10 +184,7 @@ test('Calling validate endpoint with already existing session should destroy ses ); const relative = getBackendResetUrl(url); - await request - .get(relative) - .expect(200) - .expect('Content-Type', /json/); + await request.get(relative).expect(200).expect('Content-Type', /json/); await request.get('/api/admin/features').expect(401); // we no longer should have a valid session await destroy(); }); @@ -208,7 +202,7 @@ test('Calling reset endpoint with already existing session should logout/destroy .get(relative) .expect(200) .expect('Content-Type', /json/) - .expect(res => { + .expect((res) => { token = res.body.token; }); await request @@ -237,7 +231,7 @@ test('Trying to change password with an invalid token should yield 401', async ( token: 'abc123', password, }) - .expect(res => expect(res.status).toBe(401))); + .expect((res) => expect(res.status).toBe(401))); test('Trying to change password to undefined should yield 400 without crashing the server', async () => { expect.assertions(0); @@ -252,7 +246,7 @@ test('Trying to change password to undefined should yield 400 without crashing t .get(relative) .expect(200) .expect('Content-Type', /json/) - .expect(res => { + .expect((res) => { token = res.body.token; }); await app.request diff --git a/src/test/e2e/api/client/feature.e2e.test.js b/src/test/e2e/api/client/feature.e2e.test.js index 2af6bc2260..19abc276d6 100644 --- a/src/test/e2e/api/client/feature.e2e.test.js +++ b/src/test/e2e/api/client/feature.e2e.test.js @@ -91,7 +91,7 @@ test('returns four feature toggles', async () => .get('/api/client/features') .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.features.length).toBe(4); })); @@ -100,7 +100,7 @@ test('returns four feature toggles without createdAt', async () => .get('/api/client/features') .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.features[0].createdAt).toBeFalsy(); })); @@ -129,7 +129,7 @@ test('Can filter features by namePrefix', async () => { .get('/api/client/features?namePrefix=feature.') .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.features.length).toBe(1); expect(res.body.features[0].name).toBe('feature.with.variants'); }); @@ -174,12 +174,12 @@ test('Can use multiple filters', async () => { .get('/api/client/features?tag=simple:Crazy') .expect('Content-Type', /json/) .expect(200) - .expect(res => expect(res.body.features.length).toBe(2)); + .expect((res) => expect(res.body.features.length).toBe(2)); await app.request .get('/api/client/features?namePrefix=test&tag=simple:Crazy') .expect('Content-Type', /json/) .expect(200) - .expect(res => { + .expect((res) => { expect(res.body.features.length).toBe(1); expect(res.body.features[0].name).toBe('test.feature'); }); diff --git a/src/test/e2e/api/client/register.e2e.test.js b/src/test/e2e/api/client/register.e2e.test.js index 0ce8529d4d..28aba88c8f 100644 --- a/src/test/e2e/api/client/register.e2e.test.js +++ b/src/test/e2e/api/client/register.e2e.test.js @@ -92,17 +92,17 @@ test.skip('Should handle a massive bulk registration', async () => { .expect(202); } expect(clients.length).toBe(2000); - await new Promise(res => setTimeout(res, 5500)); + await new Promise((res) => setTimeout(res, 5500)); // Verify clientInstance - const notSavedInstance = await asyncFilter(clients, async c => { + const notSavedInstance = await asyncFilter(clients, async (c) => { const exists = await clientInstanceStore.exists(c); return !exists; }); expect(notSavedInstance.length).toBe(0); // Verify application - const notSavedApp = await asyncFilter(clients, async c => { + const notSavedApp = await asyncFilter(clients, async (c) => { const exists = await clientApplicationsStore.exists(c); return !exists; }); diff --git a/src/test/e2e/custom-auth.test.ts b/src/test/e2e/custom-auth.test.ts index 17b815d1a0..a09443629e 100644 --- a/src/test/e2e/custom-auth.test.ts +++ b/src/test/e2e/custom-auth.test.ts @@ -18,7 +18,7 @@ test('Using custom auth type without defining custom middleware causes default D await request .get('/api/admin/features') .expect(401) - .expect(res => { + .expect((res) => { expect(res.body.error).toBe( 'You have to configure a custom authentication middleware. Read https://docs.getunleash.io/docs/deploy/configuring_unleash for more details', ); diff --git a/src/test/e2e/health.e2e.test.js b/src/test/e2e/health.e2e.test.ts similarity index 72% rename from src/test/e2e/health.e2e.test.js rename to src/test/e2e/health.e2e.test.ts index 0e34a35b01..be6dcef476 100644 --- a/src/test/e2e/health.e2e.test.js +++ b/src/test/e2e/health.e2e.test.ts @@ -1,8 +1,6 @@ -'use strict'; - -const { setupApp } = require('./helpers/test-helper'); -const dbInit = require('./helpers/database-init'); -const getLogger = require('../fixtures/no-logger'); +import { setupApp } from './helpers/test-helper'; +import dbInit from './helpers/database-init'; +import getLogger from '../fixtures/no-logger'; let stores; let db; diff --git a/src/test/e2e/helpers/database-init.ts b/src/test/e2e/helpers/database-init.ts index fcca99ab59..b5d0dffbd3 100644 --- a/src/test/e2e/helpers/database-init.ts +++ b/src/test/e2e/helpers/database-init.ts @@ -8,7 +8,6 @@ import dbState from './database.json'; import { LogProvider } from '../../../lib/logger'; import noLoggerProvider from '../../fixtures/no-logger'; import EnvironmentStore from '../../../lib/db/environment-store'; -import { IEnvironment } from '../../../lib/types/model'; // require('db-migrate-shared').log.silence(false); @@ -37,19 +36,19 @@ async function resetDatabase(knex) { } function createStrategies(store) { - return dbState.strategies.map(s => store.createStrategy(s)); + return dbState.strategies.map((s) => store.createStrategy(s)); } function createContextFields(store) { - return dbState.contextFields.map(c => store.create(c)); + return dbState.contextFields.map((c) => store.create(c)); } function createProjects(store) { - return dbState.projects.map(i => store.create(i)); + return dbState.projects.map((i) => store.create(i)); } function createTagTypes(store) { - return dbState.tag_types.map(t => store.createTagType(t)); + return dbState.tag_types.map((t) => store.createTagType(t)); } async function connectProject(store: EnvironmentStore): Promise { @@ -57,7 +56,7 @@ async function connectProject(store: EnvironmentStore): Promise { } async function createEnvironments(store: EnvironmentStore): Promise { - await Promise.all(dbState.environments.map(async e => store.upsert(e))); + await Promise.all(dbState.environments.map(async (e) => store.upsert(e))); } async function setupDatabase(stores) { @@ -91,16 +90,17 @@ export default async function init( // @ts-ignore await migrator({ ...config, databaseSchema: config.db.schema }); await db.destroy(); - const stores = await createStores(config, eventBus); + const testDb = createDb(config); + const stores = await createStores(config, eventBus, testDb); stores.clientMetricsStore.setMaxListeners(0); stores.eventStore.setMaxListeners(0); - await resetDatabase(stores.db); + await resetDatabase(testDb); await setupDatabase(stores); return { stores, reset: async () => { - await resetDatabase(stores.db); + await resetDatabase(testDb); await setupDatabase(stores); }, destroy: async () => { @@ -108,7 +108,7 @@ export default async function init( return new Promise((resolve, reject) => { clientInstanceStore.destroy(); clientMetricsStore.destroy(); - stores.db.destroy(error => (error ? reject(error) : resolve())); + testDb.destroy((error) => (error ? reject(error) : resolve())); }); }, }; diff --git a/src/test/e2e/helpers/test-helper.js b/src/test/e2e/helpers/test-helper.js deleted file mode 100644 index 8204900cdc..0000000000 --- a/src/test/e2e/helpers/test-helper.js +++ /dev/null @@ -1,63 +0,0 @@ -'use strict'; - -process.env.NODE_ENV = 'test'; -/* eslint-disable-next-line */ -const supertest = require('supertest'); - -const getApp = require('../../../lib/app'); -const { createTestConfig } = require('../../config/test-config'); -const { IAuthType } = require('../../../lib/types/option'); -const { createServices } = require('../../../lib/services'); - -function createApp( - stores, - adminAuthentication = IAuthType.NONE, - preHook, - customOptions, -) { - const config = createTestConfig({ - authentication: { - type: adminAuthentication, - customAuthHandler: preHook, - }, - server: { - unleashUrl: 'http://localhost:4242', - }, - ...customOptions, - }); - const services = createServices(stores, config); - - const app = getApp(config, stores, services); - const request = supertest.agent(app); - - const destroy = async () => { - services.versionService.destroy(); - services.clientMetricsService.destroy(); - services.apiTokenService.destroy(); - }; - - // TODO: use create from server-impl instead? - return { request, destroy, services }; -} - -module.exports = { - async setupApp(stores) { - return createApp(stores); - }, - - async setupAppWithAuth(stores) { - return createApp(stores, IAuthType.DEMO); - }, - - async setupAppWithCustomAuth(stores, preHook) { - return createApp(stores, IAuthType.CUSTOM, preHook); - }, - async setupAppWithBaseUrl(stores) { - return createApp(stores, undefined, undefined, { - server: { - unleashUrl: 'http://localhost:4242', - basePathUri: '/hosted', - }, - }); - }, -}; diff --git a/src/test/e2e/helpers/test-helper.ts b/src/test/e2e/helpers/test-helper.ts new file mode 100644 index 0000000000..ac8415f3af --- /dev/null +++ b/src/test/e2e/helpers/test-helper.ts @@ -0,0 +1,84 @@ +import supertest from 'supertest'; + +import EventEmitter from 'events'; +import getApp from '../../../lib/app'; +import { createTestConfig } from '../../config/test-config'; +import { IAuthType } from '../../../lib/types/option'; +import { createServices } from '../../../lib/services'; +import sessionDb from '../../../lib/middleware/session-db'; +import { IUnleashStores } from '../../../lib/types'; +import { IUnleashServices } from '../../../lib/types/services'; + +process.env.NODE_ENV = 'test'; + +export interface IUnleashTest { + request: supertest.SuperAgentTest; + destroy: () => Promise; + services: IUnleashServices; +} + +function createApp( + stores, + adminAuthentication = IAuthType.NONE, + preHook?: Function, + customOptions?: any, +): IUnleashTest { + const config = createTestConfig({ + authentication: { + type: adminAuthentication, + customAuthHandler: preHook, + }, + server: { + unleashUrl: 'http://localhost:4242', + }, + ...customOptions, + }); + const services = createServices(stores, config); + const unleashSession = sessionDb(config, undefined); + const emitter = new EventEmitter(); + emitter.setMaxListeners(0); + const app = getApp( + config, + stores, + services, + new EventEmitter(), + unleashSession, + ); + const request = supertest.agent(app); + + const destroy = async () => { + services.versionService.destroy(); + services.clientMetricsService.destroy(); + services.apiTokenService.destroy(); + }; + + // TODO: use create from server-impl instead? + return { request, destroy, services }; +} + +export async function setupApp(stores: IUnleashStores): Promise { + return createApp(stores); +} + +export async function setupAppWithAuth( + stores: IUnleashStores, +): Promise { + return createApp(stores, IAuthType.DEMO); +} + +export async function setupAppWithCustomAuth( + stores: IUnleashStores, + preHook: Function, +): Promise { + return createApp(stores, IAuthType.CUSTOM, preHook); +} +export async function setupAppWithBaseUrl( + stores: IUnleashStores, +): Promise { + return createApp(stores, undefined, undefined, { + server: { + unleashUrl: 'http://localhost:4242', + basePathUri: '/hosted', + }, + }); +} diff --git a/src/test/e2e/services/access-service.e2e.test.js b/src/test/e2e/services/access-service.e2e.test.ts similarity index 93% rename from src/test/e2e/services/access-service.e2e.test.js rename to src/test/e2e/services/access-service.e2e.test.ts index edcf8bddde..be473647a9 100644 --- a/src/test/e2e/services/access-service.e2e.test.js +++ b/src/test/e2e/services/access-service.e2e.test.ts @@ -1,13 +1,14 @@ -const dbInit = require('../helpers/database-init'); -const getLogger = require('../../fixtures/no-logger'); +import dbInit from '../helpers/database-init'; +import getLogger from '../../fixtures/no-logger'; // eslint-disable-next-line import/no-unresolved -const { +import { AccessService, - RoleName, ALL_PROJECTS, -} = require('../../../lib/services/access-service'); -const permissions = require('../../../lib/types/permissions'); +} from '../../../lib/services/access-service'; + +import * as permissions from '../../../lib/types/permissions'; +import { RoleName } from '../../../lib/types/model'; let db; let stores; @@ -42,9 +43,9 @@ beforeAll(async () => { // projectStore = stores.projectStore; accessService = new AccessService(stores, { getLogger }); const roles = await accessService.getRootRoles(); - editorRole = roles.find(r => r.name === RoleName.EDITOR); - adminRole = roles.find(r => r.name === RoleName.ADMIN); - readRole = roles.find(r => r.name === RoleName.VIEWER); + editorRole = roles.find((r) => r.name === RoleName.EDITOR); + adminRole = roles.find((r) => r.name === RoleName.ADMIN); + readRole = roles.find((r) => r.name === RoleName.VIEWER); editorUser = await createUserEditorAccess('Bob Test', 'bob@getunleash.io'); superUser = await createSuperUser(); @@ -71,11 +72,8 @@ test('should have access to admin strategies', async () => { }); test('should have access to admin contexts', async () => { - const { - CREATE_CONTEXT_FIELD, - UPDATE_CONTEXT_FIELD, - DELETE_CONTEXT_FIELD, - } = permissions; + const { CREATE_CONTEXT_FIELD, UPDATE_CONTEXT_FIELD, DELETE_CONTEXT_FIELD } = + permissions; const user = editorUser; expect(await accessService.hasPermission(user, CREATE_CONTEXT_FIELD)).toBe( true, @@ -273,7 +271,7 @@ test('should grant user access to project', async () => { const roles = await accessService.getRolesForProject(project); const projectRole = roles.find( - r => r.name === 'Member' && r.project === project, + (r) => r.name === 'Member' && r.project === project, ); await accessService.addUserToRole(sUser.id, projectRole.id); @@ -310,7 +308,7 @@ test('should not get access if not specifying project', async () => { const roles = await accessService.getRolesForProject(project); const projectRole = roles.find( - r => r.name === 'Member' && r.project === project, + (r) => r.name === 'Member' && r.project === project, ); await accessService.addUserToRole(sUser.id, projectRole.id); @@ -358,8 +356,10 @@ test('should return role with users', async () => { expect(roleWithUsers.role.name).toBe(RoleName.EDITOR); expect(roleWithUsers.users.length > 2).toBe(true); - expect(roleWithUsers.users.find(u => u.id === user.id)).toBeTruthy(); - expect(roleWithUsers.users.find(u => u.email === user.email)).toBeTruthy(); + expect(roleWithUsers.users.find((u) => u.id === user.id)).toBeTruthy(); + expect( + roleWithUsers.users.find((u) => u.email === user.email), + ).toBeTruthy(); }); test('should return role with permissions and users', async () => { @@ -377,7 +377,7 @@ test('should return role with permissions and users', async () => { expect(roleWithPermission.permissions.length > 2).toBe(true); expect( roleWithPermission.permissions.find( - p => p.permission === permissions.CREATE_PROJECT, + (p) => p.permission === permissions.CREATE_PROJECT, ), ).toBeTruthy(); expect(roleWithPermission.users.length > 2).toBe(true); @@ -386,7 +386,7 @@ test('should return role with permissions and users', async () => { test('should return list of permissions', async () => { const p = await accessService.getPermissions(); - const findPerm = perm => p.find(_ => _.name === perm); + const findPerm = (perm) => p.find((_) => _.name === perm); const { DELETE_FEATURE, 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 0e5c46669a..cdf5195695 100644 --- a/src/test/e2e/services/api-token-service.e2e.test.ts +++ b/src/test/e2e/services/api-token-service.e2e.test.ts @@ -1,8 +1,11 @@ import dbInit from '../helpers/database-init'; import getLogger from '../../fixtures/no-logger'; import { ApiTokenService } from '../../../lib/services/api-token-service'; -import { ApiTokenType, IApiToken } from '../../../lib/db/api-token-store'; import { createTestConfig } from '../../config/test-config'; +import { + ApiTokenType, + IApiToken, +} from '../../../lib/types/stores/api-token-store'; let db; let stores; 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 9bb592ba4e..e45fd51599 100644 --- a/src/test/e2e/services/client-metrics-service.e2e.test.ts +++ b/src/test/e2e/services/client-metrics-service.e2e.test.ts @@ -1,6 +1,5 @@ -import ClientMetricsService, { - IClientApp, -} from '../../../lib/services/client-metrics'; +import ClientMetricsService from '../../../lib/services/client-metrics'; +import { IClientApp } from '../../../lib/types/model'; const faker = require('faker'); const dbInit = require('../helpers/database-init'); @@ -50,14 +49,16 @@ test('Apps registered should be announced', async () => { }; await clientMetricsService.registerClient(clientRegistration, '127.0.0.1'); await clientMetricsService.registerClient(differentClient, '127.0.0.1'); - await new Promise(res => setTimeout(res, 1200)); + await new Promise((res) => setTimeout(res, 1200)); 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, 2000)); const second = await stores.clientApplicationsStore.getUnannounced(); expect(second.length).toBe(0); const events = await stores.eventStore.getEvents(); - const appCreatedEvents = events.filter(e => e.type === APPLICATION_CREATED); + const appCreatedEvents = events.filter( + (e) => e.type === APPLICATION_CREATED, + ); expect(appCreatedEvents.length).toBe(2); }); diff --git a/src/test/e2e/services/environment-service.test.ts b/src/test/e2e/services/environment-service.test.ts index 1f9e46e7d7..a89db8c611 100644 --- a/src/test/e2e/services/environment-service.test.ts +++ b/src/test/e2e/services/environment-service.test.ts @@ -71,8 +71,11 @@ test('Can connect environment to project', async () => { variants: [], }); await service.connectProjectToEnvironment('test-connection', 'default'); - const overview = await stores.projectStore.getProjectOverview('default'); - overview.forEach(f => { + const overview = await stores.projectStore.getProjectOverview( + 'default', + false, + ); + overview.forEach((f) => { expect(f.environments).toEqual([ { name: 'test-connection', @@ -90,9 +93,12 @@ test('Can remove environment from project', async () => { }); await service.removeEnvironmentFromProject('test-connection', 'default'); await service.connectProjectToEnvironment('removal-test', 'default'); - let overview = await stores.projectStore.getProjectOverview('default'); + let overview = await stores.projectStore.getProjectOverview( + 'default', + false, + ); expect(overview.length).toBeGreaterThan(0); - overview.forEach(f => { + overview.forEach((f) => { expect(f.environments).toEqual([ { name: 'removal-test', @@ -102,9 +108,9 @@ test('Can remove environment from project', async () => { ]); }); await service.removeEnvironmentFromProject('removal-test', 'default'); - overview = await stores.projectStore.getProjectOverview('default'); + overview = await stores.projectStore.getProjectOverview('default', false); expect(overview.length).toBeGreaterThan(0); - overview.forEach(o => { + overview.forEach((o) => { expect(o.environments).toEqual([]); }); }); diff --git a/src/test/e2e/services/project-service.e2e.test.js b/src/test/e2e/services/project-service.e2e.test.ts similarity index 86% rename from src/test/e2e/services/project-service.e2e.test.js rename to src/test/e2e/services/project-service.e2e.test.ts index 75c8defd86..d69c5782ca 100644 --- a/src/test/e2e/services/project-service.e2e.test.js +++ b/src/test/e2e/services/project-service.e2e.test.ts @@ -1,12 +1,11 @@ -const dbInit = require('../helpers/database-init'); -const getLogger = require('../../fixtures/no-logger'); -const ProjectService = require('../../../lib/services/project-service'); -const { - AccessService, - RoleName, -} = require('../../../lib/services/access-service'); -const { UPDATE_PROJECT } = require('../../../lib/types/permissions'); -const NotFoundError = require('../../../lib/error/notfound-error'); +import dbInit from '../helpers/database-init'; +import getLogger from '../../fixtures/no-logger'; +import ProjectService from '../../../lib/services/project-service'; +import { AccessService } from '../../../lib/services/access-service'; +import { UPDATE_PROJECT } from '../../../lib/types/permissions'; +import NotFoundError from '../../../lib/error/notfound-error'; +import { createTestConfig } from '../../config/test-config'; +import { RoleName } from '../../../lib/types/model'; let stores; let db; @@ -22,7 +21,11 @@ beforeAll(async () => { name: 'Some Name', email: 'test@getunleash.io', }); - const config = { getLogger, experimental: { rbac: true } }; + const config = createTestConfig({ + getLogger, + // @ts-ignore + experimental: { rbac: true }, + }); accessService = new AccessService(stores, config); projectService = new ProjectService(stores, config, accessService); }); @@ -46,7 +49,7 @@ test('should list all projects', async () => { await projectService.createProject(project, user); const projects = await projectService.getProjects(); - expect(projects.length).toBe(2); + expect(projects).toHaveLength(2); }); test('should create new project', async () => { @@ -187,7 +190,7 @@ test('(TODO: v4): should create roles for new project if userId is missing', asy }); const roles = await stores.accessStore.getRolesForProject(project.id); - expect(roles.length).toBe(2); + expect(roles).toHaveLength(2); expect( await accessService.hasPermission(user, UPDATE_PROJECT, project.id), ).toBe(false); @@ -201,7 +204,7 @@ test('should create roles when project is created', async () => { }; await projectService.createProject(project, user); const roles = await stores.accessStore.getRolesForProject(project.id); - expect(roles.length).toBe(2); + expect(roles).toHaveLength(2); expect( await accessService.hasPermission(user, UPDATE_PROJECT, project.id), ).toBe(true); @@ -219,10 +222,10 @@ test('should get list of users with access to project', async () => { user, ); - const owner = roles.find(role => role.name === RoleName.OWNER); - const member = roles.find(role => role.name === RoleName.MEMBER); + const owner = roles.find((role) => role.name === RoleName.OWNER); + const member = roles.find((role) => role.name === RoleName.MEMBER); - expect(users.length).toBe(1); + expect(users).toHaveLength(1); expect(users[0].id).toBe(user.id); expect(users[0].name).toBe(user.name); expect(users[0].roleId).toBe(owner.id); @@ -247,15 +250,15 @@ test('should add a member user to the project', async () => { }); const roles = await stores.accessStore.getRolesForProject(project.id); - const memberRole = roles.find(r => r.name === RoleName.MEMBER); + const memberRole = roles.find((r) => r.name === RoleName.MEMBER); await projectService.addUser(project.id, memberRole.id, projectMember1.id); await projectService.addUser(project.id, memberRole.id, projectMember2.id); const { users } = await projectService.getUsersWithAccess(project.id, user); - const memberUsers = users.filter(u => u.roleId === memberRole.id); + const memberUsers = users.filter((u) => u.roleId === memberRole.id); - expect(memberUsers.length).toBe(2); + expect(memberUsers).toHaveLength(2); expect(memberUsers[0].id).toBe(projectMember1.id); expect(memberUsers[0].name).toBe(projectMember1.name); expect(memberUsers[1].id).toBe(projectMember2.id); @@ -282,16 +285,16 @@ test('should add admin users to the project', async () => { const projectRoles = await stores.accessStore.getRolesForProject( project.id, ); - const ownerRole = projectRoles.find(r => r.name === RoleName.OWNER); + const ownerRole = projectRoles.find((r) => r.name === RoleName.OWNER); await projectService.addUser(project.id, ownerRole.id, projectAdmin1.id); await projectService.addUser(project.id, ownerRole.id, projectAdmin2.id); const { users } = await projectService.getUsersWithAccess(project.id, user); - const adminUsers = users.filter(u => u.roleId === ownerRole.id); + const adminUsers = users.filter((u) => u.roleId === ownerRole.id); - expect(adminUsers.length).toBe(3); + expect(adminUsers).toHaveLength(3); expect(adminUsers[1].id).toBe(projectAdmin1.id); expect(adminUsers[1].name).toBe(projectAdmin1.name); expect(adminUsers[2].id).toBe(projectAdmin2.id); @@ -300,7 +303,7 @@ test('should add admin users to the project', async () => { test('add user only accept to add users to project roles', async () => { const roles = await accessService.getRoles(); - const memberRole = roles.find(r => r.name === RoleName.MEMBER); + const memberRole = roles.find((r) => r.name === RoleName.MEMBER); await expect(async () => { await projectService.addUser('some-id', memberRole.id, user.id); @@ -321,7 +324,7 @@ test('add user should fail if user already have access', async () => { }); const roles = await stores.accessStore.getRolesForProject(project.id); - const memberRole = roles.find(r => r.name === RoleName.MEMBER); + const memberRole = roles.find((r) => r.name === RoleName.MEMBER); await projectService.addUser(project.id, memberRole.id, projectMember1.id); @@ -346,7 +349,7 @@ test('should remove user from the project', async () => { }); const roles = await stores.accessStore.getRolesForProject(project.id); - const memberRole = roles.find(r => r.name === RoleName.MEMBER); + const memberRole = roles.find((r) => r.name === RoleName.MEMBER); await projectService.addUser(project.id, memberRole.id, projectMember1.id); await projectService.removeUser( @@ -356,9 +359,9 @@ test('should remove user from the project', async () => { ); const { users } = await projectService.getUsersWithAccess(project.id, user); - const memberUsers = users.filter(u => u.roleId === memberRole.id); + const memberUsers = users.filter((u) => u.roleId === memberRole.id); - expect(memberUsers.length).toBe(0); + expect(memberUsers).toHaveLength(0); }); test('should not remove user from the project', async () => { @@ -370,7 +373,7 @@ test('should not remove user from the project', async () => { await projectService.createProject(project, user); const roles = await stores.accessStore.getRolesForProject(project.id); - const ownerRole = roles.find(r => r.name === RoleName.OWNER); + const ownerRole = roles.find((r) => r.name === RoleName.OWNER); await expect(async () => { await projectService.removeUser(project.id, ownerRole.id, user.id); diff --git a/src/test/e2e/services/reset-token-service.e2e.test.ts b/src/test/e2e/services/reset-token-service.e2e.test.ts index 880dcab801..4b9c173352 100644 --- a/src/test/e2e/services/reset-token-service.e2e.test.ts +++ b/src/test/e2e/services/reset-token-service.e2e.test.ts @@ -3,20 +3,19 @@ import getLogger from '../../fixtures/no-logger'; import ResetTokenService from '../../../lib/services/reset-token-service'; import UserService from '../../../lib/services/user-service'; import { AccessService } from '../../../lib/services/access-service'; -import NotFoundError from '../../../lib/error/notfound-error'; import { EmailService } from '../../../lib/services/email-service'; -import User from '../../../lib/types/user'; import { IUnleashConfig } from '../../../lib/types/option'; import { createTestConfig } from '../../config/test-config'; import SessionService from '../../../lib/services/session-service'; import InvalidTokenError from '../../../lib/error/invalid-token-error'; +import { IUser } from '../../../lib/types/user'; const config: IUnleashConfig = createTestConfig(); let stores; let db; let adminUser; -let userToCreateResetFor: User; +let userToCreateResetFor: IUser; let userIdToCreateResetFor: number; let accessService: AccessService; let userService: UserService; diff --git a/src/test/e2e/services/setting-service.test.ts b/src/test/e2e/services/setting-service.test.ts new file mode 100644 index 0000000000..4f8042f2b7 --- /dev/null +++ b/src/test/e2e/services/setting-service.test.ts @@ -0,0 +1,35 @@ +import SettingService from '../../../lib/services/setting-service'; +import { createTestConfig } from '../../config/test-config'; +import dbInit from '../helpers/database-init'; +import { IUnleashStores } from '../../../lib/types/stores'; + +let stores: IUnleashStores; +let db; +let service: SettingService; + +beforeAll(async () => { + const config = createTestConfig(); + db = await dbInit('setting_service_serial', config.getLogger); + stores = db.stores; + service = new SettingService(stores, config); +}); +afterAll(async () => { + await db.destroy(); +}); + +test('Can create new setting', async () => { + const someData = { some: 'blob' }; + await service.insert('some-setting', someData); + const actual = await service.get('some-setting'); + + expect(actual).toStrictEqual(someData); +}); + +test('Can delete setting', async () => { + const someData = { some: 'blob' }; + await service.insert('some-setting', someData); + await service.delete('some-setting'); + + const actual = await service.get('some-setting'); + expect(actual).toBeUndefined(); +}); diff --git a/src/test/e2e/services/user-service.e2e.test.ts b/src/test/e2e/services/user-service.e2e.test.ts index 3afb185cc3..96ab48039c 100644 --- a/src/test/e2e/services/user-service.e2e.test.ts +++ b/src/test/e2e/services/user-service.e2e.test.ts @@ -1,14 +1,15 @@ import dbInit from '../helpers/database-init'; import getLogger from '../../fixtures/no-logger'; import UserService from '../../../lib/services/user-service'; -import { AccessService, RoleName } from '../../../lib/services/access-service'; +import { AccessService } from '../../../lib/services/access-service'; import UserStore from '../../../lib/db/user-store'; -import { IRole } from '../../../lib/db/access-store'; import ResetTokenService from '../../../lib/services/reset-token-service'; import { EmailService } from '../../../lib/services/email-service'; import { createTestConfig } from '../../config/test-config'; import SessionService from '../../../lib/services/session-service'; import NotFoundError from '../../../lib/error/notfound-error'; +import { IRole } from '../../../lib/types/stores/access-store'; +import { RoleName } from '../../../lib/types/model'; let db; let stores; @@ -34,7 +35,7 @@ beforeAll(async () => { }); userStore = stores.userStore; const rootRoles = await accessService.getRootRoles(); - adminRole = rootRoles.find(r => r.name === RoleName.ADMIN); + adminRole = rootRoles.find((r) => r.name === RoleName.ADMIN); }); afterAll(async () => { @@ -112,7 +113,7 @@ test('should get user with root role by name', async () => { expect(user.rootRole).toBe(adminRole.id); }); -test(`deleting a user should delete the user's sessions`, async () => { +test("deleting a user should delete the user's sessions", async () => { const email = 'some@test.com'; const user = await userService.createUser({ email, diff --git a/src/test/e2e/stores/client-application-store.e2e.test.js b/src/test/e2e/stores/client-application-store.e2e.test.ts similarity index 96% rename from src/test/e2e/stores/client-application-store.e2e.test.js rename to src/test/e2e/stores/client-application-store.e2e.test.ts index 98cf106cb4..1a44f087fe 100644 --- a/src/test/e2e/stores/client-application-store.e2e.test.js +++ b/src/test/e2e/stores/client-application-store.e2e.test.ts @@ -1,8 +1,6 @@ -'use strict'; - -const faker = require('faker'); -const dbInit = require('../helpers/database-init'); -const getLogger = require('../../fixtures/no-logger'); +import faker from 'faker'; +import dbInit from '../helpers/database-init'; +import getLogger from '../../fixtures/no-logger'; let db; let stores; @@ -149,13 +147,13 @@ test('Multi row merge also works', async () => { clients.push(clientRegistration); } await clientApplicationsStore.bulkUpsert(clients); - const alteredClients = clients.map(c => ({ + const alteredClients = clients.map((c) => ({ appName: c.appName, icon: 'red', })); await clientApplicationsStore.bulkUpsert(alteredClients); const stored = await Promise.all( - clients.map(async c => + clients.map(async (c) => clientApplicationsStore.getApplication(c.appName), ), ); diff --git a/src/test/e2e/stores/event-store.e2e.test.js b/src/test/e2e/stores/event-store.e2e.test.js deleted file mode 100644 index 25ad9e6a51..0000000000 --- a/src/test/e2e/stores/event-store.e2e.test.js +++ /dev/null @@ -1,111 +0,0 @@ -'use strict'; - -const { - APPLICATION_CREATED, - FEATURE_CREATED, -} = require('../../../lib/types/events'); - -const dbInit = require('../helpers/database-init'); -const getLogger = require('../../fixtures/no-logger'); - -let db; -let stores; -let eventStore; - -beforeAll(async () => { - db = await dbInit('event_store_serial', getLogger); - stores = db.stores; - eventStore = stores.eventStore; -}); - -afterAll(async () => { - if (db) { - await db.destroy(); - } -}); -test('Should include id and createdAt when saving', async () => { - jest.useFakeTimers('modern'); - const event1 = { - type: APPLICATION_CREATED, - createdBy: '127.0.0.1', - data: { - clientIp: '127.0.0.1', - appName: 'test1', - }, - }; - const seen = []; - eventStore.on(APPLICATION_CREATED, e => seen.push(e)); - await eventStore.store(event1); - jest.advanceTimersByTime(100); - expect(seen).toHaveLength(1); - expect(seen[0].id).toBeTruthy(); - expect(seen[0].createdAt).toBeTruthy(); - expect(seen[0].data.clientIp).toBe(event1.data.clientIp); - expect(seen[0].data.appName).toBe(event1.data.appName); - jest.useRealTimers(); -}); - -test('Should include empty tags array for new event', async () => { - expect.assertions(2); - const event = { - type: FEATURE_CREATED, - createdBy: 'me@mail.com', - data: { - name: 'someName', - enabled: true, - strategies: [{ name: 'default' }], - }, - }; - - const promise = new Promise(resolve => { - eventStore.on(FEATURE_CREATED, storedEvent => { - expect(storedEvent.name).toBe(event.name); - expect(Array.isArray(storedEvent.tags)).toBe(true); - resolve(); - }); - }); - - // Trigger - await eventStore.store(event); - - return promise; -}); - -test('Should be able to store multiple events at once', async () => { - jest.useFakeTimers('modern'); - const event1 = { - type: APPLICATION_CREATED, - createdBy: '127.0.0.1', - data: { - clientIp: '127.0.0.1', - appName: 'test1', - }, - }; - const event2 = { - type: APPLICATION_CREATED, - createdBy: '127.0.0.1', - data: { - clientIp: '127.0.0.1', - appName: 'test2', - }, - }; - const event3 = { - type: APPLICATION_CREATED, - createdBy: '127.0.0.1', - data: { - clientIp: '127.0.0.1', - appName: 'test3', - }, - tags: [{ type: 'simple', value: 'mytest' }], - }; - const seen = []; - eventStore.on(APPLICATION_CREATED, e => seen.push(e)); - await eventStore.batchStore([event1, event2, event3]); - await jest.advanceTimersByTime(100); - expect(seen.length).toBe(3); - seen.forEach(e => { - expect(e.id).toBeTruthy(); - expect(e.createdAt).toBeTruthy(); - }); - jest.useRealTimers(); -}); diff --git a/src/test/e2e/stores/event-store.e2e.test.ts b/src/test/e2e/stores/event-store.e2e.test.ts new file mode 100644 index 0000000000..1c903c9e4f --- /dev/null +++ b/src/test/e2e/stores/event-store.e2e.test.ts @@ -0,0 +1,180 @@ +import { + APPLICATION_CREATED, + FEATURE_CREATED, +} from '../../../lib/types/events'; + +import dbInit from '../helpers/database-init'; +import getLogger from '../../fixtures/no-logger'; +import { IEvent } from '../../../lib/types/model'; +import { IEventStore } from '../../../lib/types/stores/event-store'; +import { IUnleashStores } from '../../../lib/types'; + +let db; +let stores: IUnleashStores; +let eventStore: IEventStore; + +beforeAll(async () => { + db = await dbInit('event_store_serial', getLogger); + stores = db.stores; + eventStore = stores.eventStore; +}); + +afterAll(async () => { + if (db) { + await db.destroy(); + } +}); +test('Should include id and createdAt when saving', async () => { + jest.useFakeTimers('modern'); + const event1 = { + type: APPLICATION_CREATED, + createdBy: '127.0.0.1', + data: { + clientIp: '127.0.0.1', + appName: 'test1', + }, + }; + const seen = []; + eventStore.on(APPLICATION_CREATED, (e) => seen.push(e)); + await eventStore.store(event1); + jest.advanceTimersByTime(100); + expect(seen).toHaveLength(1); + expect(seen[0].id).toBeTruthy(); + expect(seen[0].createdAt).toBeTruthy(); + expect(seen[0].data.clientIp).toBe(event1.data.clientIp); + expect(seen[0].data.appName).toBe(event1.data.appName); + jest.useRealTimers(); +}); + +test('Should include empty tags array for new event', async () => { + expect.assertions(2); + const event = { + type: FEATURE_CREATED, + createdBy: 'me@mail.com', + data: { + name: 'someName', + enabled: true, + strategies: [{ name: 'default' }], + }, + }; + + const promise = new Promise((resolve) => { + eventStore.on(FEATURE_CREATED, (storedEvent: IEvent) => { + expect(storedEvent.data.name).toBe(event.data.name); + expect(Array.isArray(storedEvent.tags)).toBe(true); + resolve(); + }); + }); + + // Trigger + await eventStore.store(event); + + return promise; +}); + +test('Should be able to store multiple events at once', async () => { + jest.useFakeTimers('modern'); + const event1 = { + type: APPLICATION_CREATED, + createdBy: '127.0.0.1', + data: { + clientIp: '127.0.0.1', + appName: 'test1', + }, + }; + const event2 = { + type: APPLICATION_CREATED, + createdBy: '127.0.0.1', + data: { + clientIp: '127.0.0.1', + appName: 'test2', + }, + }; + const event3 = { + type: APPLICATION_CREATED, + createdBy: '127.0.0.1', + data: { + clientIp: '127.0.0.1', + appName: 'test3', + }, + tags: [{ type: 'simple', value: 'mytest' }], + }; + const seen = []; + eventStore.on(APPLICATION_CREATED, (e) => seen.push(e)); + await eventStore.batchStore([event1, event2, event3]); + await jest.advanceTimersByTime(100); + expect(seen.length).toBe(3); + seen.forEach((e) => { + expect(e.id).toBeTruthy(); + expect(e.createdAt).toBeTruthy(); + }); + jest.useRealTimers(); +}); + +test('Should get all stored events', async () => { + const event = { + type: FEATURE_CREATED, + createdBy: 'me@mail.com', + data: { + name: 'someName', + enabled: true, + strategies: [{ name: 'default' }], + }, + }; + await eventStore.store(event); + const events = await eventStore.getAll(); + const lastEvent = events[0]; + + expect(lastEvent.type).toBe(event.type); + expect(lastEvent.createdBy).toBe(event.createdBy); +}); + +test('Should delete stored event', async () => { + const event = { + type: FEATURE_CREATED, + createdBy: 'me@mail.com', + data: { + name: 'someName', + enabled: true, + strategies: [{ name: 'default' }], + }, + }; + await eventStore.store(event); + const events = await eventStore.getAll(); + const lastEvent = events[0]; + await eventStore.delete(lastEvent.id); + + const eventsAfterDelete = await eventStore.getAll(); + const lastEventAfterDelete = eventsAfterDelete[0]; + + expect(events.length - eventsAfterDelete.length).toBe(1); + expect(lastEventAfterDelete.id).not.toBe(lastEvent.id); +}); + +test('Should get stored event by id', async () => { + const event = { + type: FEATURE_CREATED, + createdBy: 'me@mail.com', + data: { + name: 'someName', + enabled: true, + strategies: [{ name: 'default' }], + }, + }; + await eventStore.store(event); + const events = await eventStore.getAll(); + const lastEvent = events[0]; + const exists = await eventStore.exists(lastEvent.id); + const byId = await eventStore.get(lastEvent.id); + + expect(lastEvent).toStrictEqual(byId); + expect(exists).toBe(true); +}); + +test('Should delete all stored events', async () => { + await eventStore.deleteAll(); + + const events = await eventStore.getAll(); + + expect(events).toHaveLength(0); +}); diff --git a/src/test/e2e/stores/feature-tag-store.e2e.test.ts b/src/test/e2e/stores/feature-tag-store.e2e.test.ts new file mode 100644 index 0000000000..f739e7a771 --- /dev/null +++ b/src/test/e2e/stores/feature-tag-store.e2e.test.ts @@ -0,0 +1,109 @@ +import { IFeatureTagStore } from 'lib/types/stores/feature-tag-store'; +import { IFeatureToggleStore } from 'lib/types/stores/feature-toggle-store'; +import dbInit from '../helpers/database-init'; +import getLogger from '../../fixtures/no-logger'; + +let stores; +let db; +let featureTagStore: IFeatureTagStore; +let featureToggleStore: IFeatureToggleStore; + +const featureName = 'test-tag'; +const tag = { type: 'simple', value: 'test' }; + +beforeAll(async () => { + db = await dbInit('feature_tag_store_serial', getLogger); + stores = db.stores; + featureTagStore = stores.featureTagStore; + featureToggleStore = stores.featureToggleStore; + await stores.tagStore.createTag(tag); + await featureToggleStore.createFeature('default', { name: featureName }); +}); + +afterAll(async () => { + await stores.tagStore.delete(tag); + await db.destroy(); +}); + +afterEach(async () => { + await featureTagStore.deleteAll(); +}); + +test('should tag feature', async () => { + await featureTagStore.tagFeature(featureName, tag); + const featureTags = await featureTagStore.getAllTagsForFeature(featureName); + const featureTag = await featureTagStore.get({ + featureName, + tagType: tag.type, + tagValue: tag.value, + }); + expect(featureTags).toHaveLength(1); + expect(featureTags[0]).toStrictEqual(tag); + expect(featureTag.featureName).toBe(featureName); + expect(featureTag.tagValue).toBe(tag.value); +}); + +test('feature tag exits', async () => { + await featureTagStore.tagFeature(featureName, tag); + const exists = await featureTagStore.exists({ + featureName, + tagType: tag.type, + tagValue: tag.value, + }); + expect(exists).toBe(true); +}); + +test('should delete feature tag', async () => { + await featureTagStore.tagFeature(featureName, tag); + await featureTagStore.delete({ + featureName, + tagType: tag.type, + tagValue: tag.value, + }); + const featureTags = await featureTagStore.getAllTagsForFeature(featureName); + expect(featureTags).toHaveLength(0); +}); + +test('should untag feature', async () => { + await featureTagStore.tagFeature(featureName, tag); + await featureTagStore.untagFeature(featureName, tag); + const featureTags = await featureTagStore.getAllTagsForFeature(featureName); + expect(featureTags).toHaveLength(0); +}); + +test('should throw if feature have tag', async () => { + expect.assertions(1); + await featureTagStore.tagFeature(featureName, tag); + try { + await featureTagStore.tagFeature(featureName, tag); + } catch (e) { + expect(e.message).toContain('already had the tag'); + } +}); + +test('get all feature tags', async () => { + await featureTagStore.tagFeature(featureName, tag); + await featureToggleStore.createFeature('default', { + name: 'some-other-toggle', + }); + await featureTagStore.tagFeature('some-other-toggle', tag); + const all = await featureTagStore.getAll(); + expect(all).toHaveLength(2); +}); + +test('should import feature tags', async () => { + await featureToggleStore.createFeature('default', { + name: 'some-other-toggle-import', + }); + await featureTagStore.importFeatureTags([ + { featureName, tagType: tag.type, tagValue: tag.value }, + { + featureName: 'some-other-toggle-import', + tagType: tag.type, + tagValue: tag.value, + }, + ]); + + const all = await featureTagStore.getAll(); + expect(all).toHaveLength(2); +}); diff --git a/src/test/e2e/stores/feature-toggle-store.e2e.test.js b/src/test/e2e/stores/feature-toggle-store.e2e.test.ts similarity index 84% rename from src/test/e2e/stores/feature-toggle-store.e2e.test.js rename to src/test/e2e/stores/feature-toggle-store.e2e.test.ts index 8582f483e9..004efba21c 100644 --- a/src/test/e2e/stores/feature-toggle-store.e2e.test.js +++ b/src/test/e2e/stores/feature-toggle-store.e2e.test.ts @@ -1,7 +1,5 @@ -'use strict'; - -const dbInit = require('../helpers/database-init'); -const getLogger = require('../../fixtures/no-logger'); +import dbInit from '../helpers/database-init'; +import getLogger from '../../fixtures/no-logger'; let stores; let db; diff --git a/src/test/e2e/stores/feature-type-store.e2e.test.ts b/src/test/e2e/stores/feature-type-store.e2e.test.ts new file mode 100644 index 0000000000..dceebde442 --- /dev/null +++ b/src/test/e2e/stores/feature-type-store.e2e.test.ts @@ -0,0 +1,41 @@ +import { IFeatureTypeStore } from 'lib/types/stores/feature-type-store'; +import dbInit from '../helpers/database-init'; +import getLogger from '../../fixtures/no-logger'; + +let stores; +let db; +let featureTypeStore: IFeatureTypeStore; + +beforeAll(async () => { + db = await dbInit('feature_type_store_serial', getLogger); + stores = db.stores; + featureTypeStore = stores.featureTypeStore; +}); + +afterAll(async () => { + await db.destroy(); +}); + +test('should have 5 default types', async () => { + const types = await featureTypeStore.getAll(); + expect(types.length).toBe(5); + expect(types[0].name).toBe('Release'); +}); + +test('should be possible to get by name', async () => { + const type = await featureTypeStore.getByName('Experiment'); + expect(type.name).toBe('Experiment'); +}); + +test('should be possible to get by id', async () => { + const type = await featureTypeStore.exists(0); + expect(type).toBeDefined(); +}); + +test('should be possible to delete by id', async () => { + const types = await featureTypeStore.getAll(); + const deleteType = types.pop(); + await featureTypeStore.delete(deleteType.id); + const typesAfterDelete = await featureTypeStore.getAll(); + expect(typesAfterDelete.length).toBe(4); +}); diff --git a/src/test/e2e/stores/project-store.e2e.test.js b/src/test/e2e/stores/project-store.e2e.test.ts similarity index 51% rename from src/test/e2e/stores/project-store.e2e.test.js rename to src/test/e2e/stores/project-store.e2e.test.ts index d3a5b46d05..6ee6c5be0f 100644 --- a/src/test/e2e/stores/project-store.e2e.test.js +++ b/src/test/e2e/stores/project-store.e2e.test.ts @@ -1,16 +1,19 @@ -'use strict'; +import { IProjectInsert, IProjectStore } from 'lib/types/stores/project-store'; +import { IEnvironmentStore } from 'lib/types/stores/environment-store'; -const dbInit = require('../helpers/database-init'); -const getLogger = require('../../fixtures/no-logger'); +import dbInit from '../helpers/database-init'; +import getLogger from '../../fixtures/no-logger'; let stores; let db; -let projectStore; +let projectStore: IProjectStore; +let environmentStore: IEnvironmentStore; beforeAll(async () => { db = await dbInit('project_store_serial', getLogger); stores = db.stores; projectStore = stores.projectStore; + environmentStore = stores.environmentStore; }); afterAll(async () => { @@ -31,10 +34,12 @@ test('should create new project', async () => { }; await projectStore.create(project); const ret = await projectStore.get('test'); + const exists = await projectStore.exists('test'); expect(project.id).toEqual(ret.id); expect(project.name).toEqual(ret.name); expect(project.description).toEqual(ret.description); expect(ret.createdAt).toBeTruthy(); + expect(exists).toBe(true); }); test('should delete project', async () => { @@ -75,10 +80,53 @@ test('should update project', async () => { expect(updatedProject.description).toBe(readProject.description); }); -test('should give error when getting unkown project', async () => { +test('should give error when getting unknown project', async () => { try { await projectStore.get('unknown'); } catch (err) { expect(err.message).toBe('No project found'); } }); + +test('should import projects', async () => { + const projectsCount = (await projectStore.getAll()).length; + + const projectsToImport: IProjectInsert[] = [ + { description: 'some project desc', name: 'some name', id: 'someId' }, + { + description: 'another project', + name: 'another name', + id: 'anotherId', + }, + ]; + + await projectStore.importProjects(projectsToImport); + + const projects = await projectStore.getAll(); + + const someId = projects.find((p) => p.id === 'someId'); + const anotherId = projects.find((p) => p.id === 'anotherId'); + + expect(projects.length - projectsCount).toBe(2); + expect(someId).toBeDefined(); + expect(someId.name).toBe('some name'); + expect(someId.description).toBe('some project desc'); + expect(anotherId).toBeDefined(); +}); + +test('should add environment to project', async () => { + const project = { + id: 'test-env', + name: 'New project with env', + description: 'Blah', + }; + + await environmentStore.upsert({ name: 'test', displayName: 'Test Env' }); + + await projectStore.create(project); + await projectStore.addEnvironmentToProject(project.id, 'test'); + + const envs = await projectStore.getEnvironmentsForProject(project.id); + + expect(envs).toHaveLength(1); +}); diff --git a/src/test/e2e/stores/setting-store.e2e.test.js b/src/test/e2e/stores/setting-store.e2e.test.js deleted file mode 100644 index e1a40113a3..0000000000 --- a/src/test/e2e/stores/setting-store.e2e.test.js +++ /dev/null @@ -1,28 +0,0 @@ -'use strict'; - -const dbInit = require('../helpers/database-init'); -const getLogger = require('../../fixtures/no-logger'); - -let stores; -let db; - -beforeAll(async () => { - db = await dbInit('setting_store_serial', getLogger); - stores = db.stores; -}); - -afterAll(async () => { - await db.destroy(); -}); - -test('should have api secret stored', async () => { - const secret = await stores.settingStore.get('unleash.secret'); - expect(secret).toBeDefined(); -}); - -test('should insert arbitrary value', async () => { - const value = { b: 'hello' }; - await stores.settingStore.insert('unleash.custom', value); - const ret = await stores.settingStore.get('unleash.custom'); - expect(ret).toEqual(value); -}); diff --git a/src/test/e2e/stores/setting-store.e2e.test.ts b/src/test/e2e/stores/setting-store.e2e.test.ts new file mode 100644 index 0000000000..3140b41323 --- /dev/null +++ b/src/test/e2e/stores/setting-store.e2e.test.ts @@ -0,0 +1,64 @@ +import dbInit from '../helpers/database-init'; +import getLogger from '../../fixtures/no-logger'; + +let stores; +let db; + +beforeAll(async () => { + db = await dbInit('setting_store_serial', getLogger); + stores = db.stores; +}); + +afterAll(async () => { + await db.destroy(); +}); + +test('should have api secret stored', async () => { + const secret = await stores.settingStore.get('unleash.secret'); + expect(secret).toBeDefined(); +}); + +test('should insert arbitrary value', async () => { + const value = { b: 'hello' }; + await stores.settingStore.insert('unleash.custom', value); + const ret = await stores.settingStore.get('unleash.custom'); + expect(ret).toEqual(value); +}); + +test('should update arbitrary value', async () => { + const value = { b: 'hello' }; + await stores.settingStore.insert('unleash.custom', value); + + const value2 = { some: 'other' }; + await stores.settingStore.insert('unleash.custom', value2); + const ret = await stores.settingStore.get('unleash.custom'); + expect(ret).toEqual(value2); +}); + +test('should delete arbitrary value', async () => { + const value = { b: 'hello' }; + await stores.settingStore.insert('unleash.custom', value); + await stores.settingStore.delete('unleash.custom'); + const ret = await stores.settingStore.get('unleash.custom'); + expect(ret).toBeUndefined(); +}); + +test('should getAll', async () => { + await stores.settingStore.insert('unleash.custom.1', { b: 'hello' }); + await stores.settingStore.insert('unleash.custom.2', { b: 'hello' }); + await stores.settingStore.insert('unleash.custom.3', { b: 'hello' }); + const ret = await stores.settingStore.getAll(); + expect(ret).toHaveLength(5); +}); + +test('should exists', async () => { + await stores.settingStore.insert('unleash.custom.2', { b: 'hello' }); + const ret = await stores.settingStore.exists('unleash.custom.2'); + expect(ret).toBe(true); +}); + +test('should delete all', async () => { + await stores.settingStore.deleteAll(); + const ret = await stores.settingStore.getAll(); + expect(ret).toHaveLength(0); +}); diff --git a/src/test/e2e/stores/user-feedback-store.e2e.test.ts b/src/test/e2e/stores/user-feedback-store.e2e.test.ts new file mode 100644 index 0000000000..7e6fd5bae3 --- /dev/null +++ b/src/test/e2e/stores/user-feedback-store.e2e.test.ts @@ -0,0 +1,88 @@ +import { IUserFeedbackStore } from 'lib/types/stores/user-feedback-store'; +import { IUserStore } from 'lib/types/stores/user-store'; +import dbInit from '../helpers/database-init'; +import getLogger from '../../fixtures/no-logger'; + +let stores; +let db; +let userFeedbackStore: IUserFeedbackStore; +let userStore: IUserStore; +let currentUser; + +beforeAll(async () => { + db = await dbInit('project_store_serial', getLogger); + stores = db.stores; + userFeedbackStore = stores.userFeedbackStore; + userStore = stores.userStore; + currentUser = await userStore.upsert({ email: 'me.feedback@mail.com' }); +}); + +afterAll(async () => { + await db.destroy(); +}); + +afterEach(async () => { + await userFeedbackStore.deleteAll(); +}); + +test('should create userFeedback', async () => { + await userFeedbackStore.updateFeedback({ + feedbackId: 'some-id', + userId: currentUser.id, + neverShow: false, + given: new Date(), + }); + const userFeedbacks = await userFeedbackStore.getAllUserFeedback( + currentUser.id, + ); + expect(userFeedbacks).toHaveLength(1); + expect(userFeedbacks[0].feedbackId).toBe('some-id'); +}); + +test('should get userFeedback', async () => { + await userFeedbackStore.updateFeedback({ + feedbackId: 'some-id', + userId: currentUser.id, + neverShow: false, + given: new Date(), + }); + const userFeedback = await userFeedbackStore.getFeedback( + currentUser.id, + 'some-id', + ); + expect(userFeedback.feedbackId).toBe('some-id'); +}); + +test('should exists', async () => { + await userFeedbackStore.updateFeedback({ + feedbackId: 'some-id-3', + userId: currentUser.id, + neverShow: false, + given: new Date(), + }); + const exists = await userFeedbackStore.exists({ + userId: currentUser.id, + feedbackId: 'some-id-3', + }); + expect(exists).toBe(true); +}); + +test('should not exists', async () => { + const exists = await userFeedbackStore.exists({ + userId: currentUser.id, + feedbackId: 'some-id-not-here', + }); + expect(exists).toBe(false); +}); + +test('should get all userFeedbacks', async () => { + await userFeedbackStore.updateFeedback({ + feedbackId: 'some-id-2', + userId: currentUser.id, + neverShow: false, + given: new Date(), + }); + const userFeedbacks = await userFeedbackStore.getAll(); + expect(userFeedbacks).toHaveLength(1); + expect(userFeedbacks[0].feedbackId).toBe('some-id-2'); +}); diff --git a/src/test/e2e/stores/user-store.e2e.test.js b/src/test/e2e/stores/user-store.e2e.test.ts similarity index 87% rename from src/test/e2e/stores/user-store.e2e.test.js rename to src/test/e2e/stores/user-store.e2e.test.ts index 622f6e693a..15aa0e65b8 100644 --- a/src/test/e2e/stores/user-store.e2e.test.js +++ b/src/test/e2e/stores/user-store.e2e.test.ts @@ -1,8 +1,6 @@ -'use strict'; - -const NotFoundError = require('../../../lib/error/notfound-error'); -const dbInit = require('../helpers/database-init'); -const getLogger = require('../../fixtures/no-logger'); +import NotFoundError from '../../../lib/error/notfound-error'; +import dbInit from '../helpers/database-init'; +import getLogger from '../../fixtures/no-logger'; let stores; let db; @@ -46,7 +44,7 @@ test('should insert new user with email and return it', async () => { test('should insert new user with username', async () => { const user = { username: 'admin' }; await stores.userStore.upsert(user); - const dbUser = await stores.userStore.get(user); + const dbUser = await stores.userStore.getByQuery(user); expect(dbUser.username).toEqual(user.username); }); @@ -80,7 +78,7 @@ test('should update loginAttempts for user', async () => { await store.upsert(user); await store.incLoginAttempts(user); await store.incLoginAttempts(user); - const storedUser = await store.get(user); + const storedUser = await store.getByQuery(user); expect(storedUser.loginAttempts).toBe(2); }); @@ -90,7 +88,7 @@ test('should not increment for user unknown user', async () => { const user = { email: 'another@mail.com' }; await store.upsert(user); await store.incLoginAttempts({ email: 'unknown@mail.com' }); - const storedUser = await store.get(user); + const storedUser = await store.getByQuery(user); expect(storedUser.loginAttempts).toBe(0); }); @@ -102,7 +100,7 @@ test('should reset user after successful login', async () => { await store.incLoginAttempts(user); await store.successfullyLogin(user); - const storedUser = await store.get(user); + const storedUser = await store.getByQuery(user); expect(storedUser.loginAttempts).toBe(0); expect(storedUser.seenAt >= user.seenAt).toBe(true); @@ -120,7 +118,7 @@ test('should only update specified fields on user', async () => { await store.upsert({ username: 'test' }); - const storedUser = await store.get({ email }); + const storedUser = await store.getByQuery({ email }); expect(storedUser.email).toEqual(user.email); expect(storedUser.username).toEqual(user.username); @@ -135,7 +133,7 @@ test('should always lowercase emails on inserts', async () => { await store.upsert(user); - const storedUser = await store.get({ email }); + const storedUser = await store.getByQuery({ email }); expect(storedUser.email).toEqual(user.email.toLowerCase()); }); @@ -149,7 +147,7 @@ test('should always lowercase emails on updates', async () => { await store.upsert(user); - let storedUser = await store.get({ email }); + let storedUser = await store.getByQuery({ email }); expect(storedUser.email).toEqual(user.email.toLowerCase()); @@ -159,6 +157,6 @@ test('should always lowercase emails on updates', async () => { }; await store.upsert(updatedUser); - storedUser = await store.get({ id: storedUser.id }); + storedUser = await store.get(storedUser.id); expect(storedUser.email).toBe(updatedUser.email.toLowerCase()); }); diff --git a/src/test/fixtures/access-service-mock.ts b/src/test/fixtures/access-service-mock.ts index e77d32a813..c8c5089bbd 100644 --- a/src/test/fixtures/access-service-mock.ts +++ b/src/test/fixtures/access-service-mock.ts @@ -1,19 +1,12 @@ /* eslint-disable @typescript-eslint/explicit-module-boundary-types */ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { IRole } from '../../lib/db/access-store'; -import { - AccessService, - IUserWithRole, - RoleName, - IPermission, - IRoleData, -} from '../../lib/services/access-service'; +import { AccessService } from '../../lib/services/access-service'; import User from '../../lib/types/user'; import noLoggerProvider from './no-logger'; +import { IRole } from '../../lib/types/stores/access-store'; +import { IPermission, IRoleData, IUserWithRole } from '../../lib/types/model'; class AccessServiceMock extends AccessService { - public roleName: RoleName; - constructor() { super( { accessStore: undefined, userStore: undefined }, diff --git a/src/test/fixtures/fake-access-store.ts b/src/test/fixtures/fake-access-store.ts index 2f71b0e6b7..0e637d03dd 100644 --- a/src/test/fixtures/fake-access-store.ts +++ b/src/test/fixtures/fake-access-store.ts @@ -1,16 +1,16 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { - AccessStore, - IRole, - IUserRole, - IUserPermission, -} from '../../lib/db/access-store'; import noLoggerProvider from './no-logger'; +import { + IAccessStore, + IRole, + IUserPermission, + IUserRole, +} from '../../lib/types/stores/access-store'; -class AccessStoreMock extends AccessStore { - constructor() { - super(undefined, undefined, noLoggerProvider); - } +class AccessStoreMock implements IAccessStore { + userPermissions: IUserPermission[] = []; + + roles: IRole[] = []; getPermissionsForUser(userId: Number): Promise { return Promise.resolve([]); @@ -40,7 +40,7 @@ class AccessStoreMock extends AccessStore { return Promise.resolve([]); } - getUserIdsForRole(roleId: number): Promise { + getUserIdsForRole(roleId: number): Promise { throw new Error('Method not implemented.'); } @@ -80,6 +80,36 @@ class AccessStoreMock extends AccessStore { getRootRoleForAllUsers(): Promise { throw new Error('Method not implemented.'); } + + delete(key: number): Promise { + return Promise.resolve(undefined); + } + + deleteAll(): Promise { + return Promise.resolve(undefined); + } + + destroy(): void {} + + exists(key: number): Promise { + return Promise.resolve(false); + } + + get(key: number): Promise { + return Promise.resolve(undefined); + } + + getAll(): Promise { + return Promise.resolve([]); + } + + getRootRoles(): Promise { + return Promise.resolve([]); + } + + removeRolesOfTypeForUser(userId: number, roleType: string): Promise { + return Promise.resolve(undefined); + } } module.exports = AccessStoreMock; diff --git a/src/test/fixtures/fake-addon-store.js b/src/test/fixtures/fake-addon-store.js deleted file mode 100644 index 6670087e80..0000000000 --- a/src/test/fixtures/fake-addon-store.js +++ /dev/null @@ -1,23 +0,0 @@ -'use strict'; - -module.exports = () => { - const _addons = []; - - return { - insert: async addon => { - const a = { id: _addons.length, ...addon }; - _addons.push(a); - return a; - }, - update: async (id, value) => { - _addons[id] = value; - Promise.resolve(value); - }, - delete: async id => { - _addons.splice(id, 1); - Promise.resolve(); - }, - get: async id => _addons[id], - getAll: () => Promise.resolve(_addons), - }; -}; diff --git a/src/test/fixtures/fake-addon-store.ts b/src/test/fixtures/fake-addon-store.ts new file mode 100644 index 0000000000..c710841a44 --- /dev/null +++ b/src/test/fixtures/fake-addon-store.ts @@ -0,0 +1,58 @@ +import { + IAddon, + IAddonDto, + IAddonStore, +} from '../../lib/types/stores/addon-store'; +import NotFoundError from '../../lib/error/notfound-error'; + +export default class FakeAddonStore implements IAddonStore { + addons: IAddon[] = []; + + highestId = 0; + + async delete(key: number): Promise { + this.addons.splice( + this.addons.findIndex((a) => a.id === key), + 1, + ); + } + + async deleteAll(): Promise { + this.addons = []; + } + + destroy(): void {} + + async exists(key: number): Promise { + return this.addons.some((a) => a.id === key); + } + + async get(key: number): Promise { + const addon = this.addons.find((a) => a.id === key); + if (addon) { + return addon; + } + throw new NotFoundError(`Could not find addon with id ${key}`); + } + + async getAll(): Promise { + return this.addons; + } + + async insert(addon: IAddonDto): Promise { + const ins: IAddon = { + id: this.highestId++, + createdAt: new Date(), + ...addon, + }; + this.addons.push(ins); + return ins; + } + + async update(id: number, addon: IAddonDto): Promise { + await this.delete(id); + const inserted: IAddon = { id, createdAt: new Date(), ...addon }; + this.addons.push(inserted); + return inserted; + } +} diff --git a/src/test/fixtures/fake-api-token-store.ts b/src/test/fixtures/fake-api-token-store.ts new file mode 100644 index 0000000000..1299431ee9 --- /dev/null +++ b/src/test/fixtures/fake-api-token-store.ts @@ -0,0 +1,67 @@ +import { + IApiToken, + IApiTokenCreate, + IApiTokenStore, +} from '../../lib/types/stores/api-token-store'; +import NotFoundError from '../../lib/error/notfound-error'; + +export default class FakeApiTokenStore implements IApiTokenStore { + tokens: IApiToken[]; + + async delete(key: string): Promise { + this.tokens.splice( + this.tokens.findIndex((t) => t.secret === key), + 1, + ); + } + + async deleteAll(): Promise { + this.tokens = []; + } + + destroy(): void {} + + async exists(key: string): Promise { + return this.tokens.some((token) => token.secret === key); + } + + async get(key: string): Promise { + const token = this.tokens.find((t) => t.secret === key); + if (token) { + return token; + } + throw new NotFoundError(`Could not find token with secret ${key}`); + } + + async getAll(): Promise { + return this.tokens; + } + + async getAllActive(): Promise { + return this.tokens.filter((token) => token.expiresAt > new Date()); + } + + async insert(newToken: IApiTokenCreate): Promise { + const apiToken = { + createdAt: new Date(), + ...newToken, + }; + this.tokens.push(apiToken); + return apiToken; + } + + async markSeenAt(secrets: string[]): Promise { + this.tokens + .filter((t) => secrets.includes(t.secret)) + .forEach((t) => { + // eslint-disable-next-line no-param-reassign + t.seenAt = new Date(); + }); + } + + async setExpiry(secret: string, expiresAt: Date): Promise { + const t = await this.get(secret); + t.expiresAt = expiresAt; + return t; + } +} diff --git a/src/test/fixtures/fake-client-applications-store.js b/src/test/fixtures/fake-client-applications-store.js deleted file mode 100644 index 76b0ff5502..0000000000 --- a/src/test/fixtures/fake-client-applications-store.js +++ /dev/null @@ -1,29 +0,0 @@ -'use strict'; - -const NotFoundError = require('../../lib/error/notfound-error'); - -module.exports = () => { - let apps = []; - - return { - upsert: app => { - apps.push(app); - return Promise.resolve(); - }, - insertNewRow: value => { - apps.push(value); - return Promise.resolve(); - }, - getApplications: () => Promise.resolve(apps), - getApplication: appName => { - const app = apps.filter(a => a.appName === appName)[0]; - if (!app) { - throw new NotFoundError(`Could not find app=${appName}`); - } - return app; - }, - deleteApplication: appName => { - apps = apps.filter(app => app.appName !== appName); - }, - }; -}; diff --git a/src/test/fixtures/fake-client-applications-store.ts b/src/test/fixtures/fake-client-applications-store.ts new file mode 100644 index 0000000000..b6b6907d7e --- /dev/null +++ b/src/test/fixtures/fake-client-applications-store.ts @@ -0,0 +1,81 @@ +import { + IClientApplication, + IClientApplicationsStore, +} from '../../lib/types/stores/client-applications-store'; +import NotFoundError from '../../lib/error/notfound-error'; +import { IApplicationQuery } from '../../lib/types/query'; + +export default class FakeClientApplicationsStore + implements IClientApplicationsStore +{ + apps: IClientApplication[] = []; + + async bulkUpsert(details: Partial[]): Promise { + // @ts-ignore + details.forEach((d) => this.apps.push(d)); + } + + async delete(key: string): Promise { + this.apps.splice( + this.apps.findIndex((a) => a.appName === key), + 1, + ); + } + + async deleteAll(): Promise { + this.apps = []; + } + + async deleteApplication(appName: string): Promise { + return this.delete(appName); + } + + destroy(): void {} + + async exists(key: string): Promise { + return this.apps.some((a) => a.appName === key); + } + + async get(key: string): Promise { + const app = this.apps.find((a) => a.appName === key); + if (app) { + return app; + } + throw new NotFoundError( + `Could not find application with appName: ${key}`, + ); + } + + async getAll(): Promise { + return this.apps; + } + + async getApplication(appName: string): Promise { + return this.get(appName); + } + + async getAppsForStrategy( + query: IApplicationQuery, + ): Promise { + if (query.strategyName) { + return this.apps.filter((a) => + a.strategies.includes(query.strategyName), + ); + } + return this.apps; + } + + async getUnannounced(): Promise { + return this.apps.filter((a) => !a.announced); + } + + async setUnannouncedToAnnounced(): Promise { + this.apps = this.apps.map((a) => ({ ...a, announced: true })); + return this.apps; + } + + async upsert(details: Partial): Promise { + await this.delete(details.appName); + return this.bulkUpsert([details]); + } +} diff --git a/src/test/fixtures/fake-client-instance-store.js b/src/test/fixtures/fake-client-instance-store.js deleted file mode 100644 index b163c5eb36..0000000000 --- a/src/test/fixtures/fake-client-instance-store.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -module.exports = () => ({ - insert: () => Promise.resolve(), - getApplications: () => Promise.resolve([]), - deleteForApplication: () => Promise.resolve(), -}); diff --git a/src/test/fixtures/fake-client-instance-store.ts b/src/test/fixtures/fake-client-instance-store.ts new file mode 100644 index 0000000000..f938d684d2 --- /dev/null +++ b/src/test/fixtures/fake-client-instance-store.ts @@ -0,0 +1,77 @@ +import { + IClientInstance, + IClientInstanceStore, + INewClientInstance, +} from '../../lib/types/stores/client-instance-store'; +import NotFoundError from '../../lib/error/notfound-error'; + +export default class FakeClientInstanceStore implements IClientInstanceStore { + instances: IClientInstance[] = []; + + async bulkUpsert(instances: INewClientInstance[]): Promise { + instances.forEach((i) => { + this.instances.push({ createdAt: new Date(), ...i }); + }); + } + + async delete( + key: Pick, + ): Promise { + this.instances.splice( + this.instances.findIndex( + (i) => + i.instanceId === key.instanceId && + i.appName === key.appName, + ), + 1, + ); + } + + async deleteAll(): Promise { + this.instances = []; + } + + async deleteForApplication(appName: string): Promise { + this.instances = this.instances.filter((i) => i.appName !== appName); + } + + destroy(): void {} + + async exists( + key: Pick, + ): Promise { + return this.instances.some( + (i) => i.appName === key.appName && i.instanceId === key.instanceId, + ); + } + + async get( + key: Pick, + ): Promise { + const instance = this.instances.find( + (i) => i.appName === key.appName && i.instanceId === key.instanceId, + ); + if (instance) { + return instance; + } + throw new NotFoundError(`Could not find instance with key: ${key}`); + } + + async getAll(): Promise { + return this.instances; + } + + async getByAppName(appName: string): Promise { + return this.instances.filter((i) => i.appName === appName); + } + + async getDistinctApplications(): Promise { + const apps = new Set(); + this.instances.forEach((i) => apps.add(i.appName)); + return Array.from(apps.values()); + } + + async insert(details: INewClientInstance): Promise { + this.instances.push({ createdAt: new Date(), ...details }); + } +} diff --git a/src/test/fixtures/fake-client-metrics-store.ts b/src/test/fixtures/fake-client-metrics-store.ts new file mode 100644 index 0000000000..c7cb5e1b85 --- /dev/null +++ b/src/test/fixtures/fake-client-metrics-store.ts @@ -0,0 +1,53 @@ +import EventEmitter from 'events'; +import { IClientMetricsStore } from '../../lib/types/stores/client-metrics-store'; +import { IClientMetric } from '../../lib/types/stores/client-metrics-db'; +import NotFoundError from '../../lib/error/notfound-error'; + +export default class FakeClientMetricsStore + extends EventEmitter + implements IClientMetricsStore +{ + metrics: IClientMetric[] = []; + + constructor() { + super(); + this.setMaxListeners(0); + } + + async getMetricsLastHour(): Promise { + return Promise.resolve([]); + } + + async insert(): Promise { + return Promise.resolve(); + } + + async delete(key: number): Promise { + this.metrics.splice( + this.metrics.findIndex((m) => m.id === key), + 1, + ); + } + + async deleteAll(): Promise { + return Promise.resolve(undefined); + } + + destroy(): void {} + + async exists(key: number): Promise { + return this.metrics.some((m) => m.id === key); + } + + async get(key: number): Promise { + const metric = this.metrics.find((m) => m.id === key); + if (metric) { + return metric; + } + throw new NotFoundError(`Could not find metric with key: ${key}`); + } + + async getAll(): Promise { + return this.metrics; + } +} diff --git a/src/test/fixtures/fake-context-field-store.ts b/src/test/fixtures/fake-context-field-store.ts new file mode 100644 index 0000000000..6dc691b6bf --- /dev/null +++ b/src/test/fixtures/fake-context-field-store.ts @@ -0,0 +1,76 @@ +import { + IContextField, + IContextFieldDto, + IContextFieldStore, +} from '../../lib/types/stores/context-field-store'; +import NotFoundError from '../../lib/error/notfound-error'; + +export default class FakeContextFieldStore implements IContextFieldStore { + defaultContextFields: IContextField[] = [ + { + name: 'environment', + createdAt: new Date(), + description: 'Environment', + sortOrder: 0, + stickiness: true, + }, + { + name: 'userId', + createdAt: new Date(), + description: 'Environment', + sortOrder: 0, + stickiness: true, + }, + { + name: 'appName', + createdAt: new Date(), + description: 'Environment', + sortOrder: 0, + stickiness: true, + }, + ]; + + contextFields: IContextField[] = this.defaultContextFields; + + async create(data: IContextFieldDto): Promise { + const cF: IContextField = { createdAt: new Date(), ...data }; + this.contextFields.push(cF); + return cF; + } + + async delete(key: string): Promise { + this.contextFields.splice( + this.contextFields.findIndex((cF) => cF.name === key), + 1, + ); + } + + async deleteAll(): Promise { + this.contextFields = this.defaultContextFields; + } + + destroy(): void {} + + async exists(key: string): Promise { + return this.contextFields.some((cF) => cF.name === key); + } + + async get(key: string): Promise { + const contextField = this.contextFields.find((cF) => cF.name === key); + if (contextField) { + return contextField; + } + throw new NotFoundError( + `Could not find contextField with name : ${key}`, + ); + } + + async getAll(): Promise { + return this.contextFields; + } + + async update(data: IContextFieldDto): Promise { + await this.delete(data.name); + return this.create(data); + } +} diff --git a/src/test/fixtures/fake-context-store.js b/src/test/fixtures/fake-context-store.js deleted file mode 100644 index b45d67a58d..0000000000 --- a/src/test/fixtures/fake-context-store.js +++ /dev/null @@ -1,31 +0,0 @@ -'use strict'; - -const NotFoundError = require('../../lib/error/notfound-error'); - -module.exports = () => { - let _contextFields = [ - { name: 'environment' }, - { name: 'userId' }, - { name: 'appName' }, - ]; - - return { - getAll: () => Promise.resolve(_contextFields), - get: name => { - const field = _contextFields.find(c => c.name === name); - if (field) { - return Promise.resolve(field); - } - return Promise.reject(NotFoundError); - }, - create: contextField => _contextFields.push(contextField), - update: field => { - _contextFields = _contextFields.map(c => - c.name === field.name ? field : c, - ); - }, - delete: name => { - _contextFields = _contextFields.filter(c => c.name !== name); - }, - }; -}; diff --git a/src/test/fixtures/fake-environment-store.ts b/src/test/fixtures/fake-environment-store.ts index 3753feaac4..eca4c77d85 100644 --- a/src/test/fixtures/fake-environment-store.ts +++ b/src/test/fixtures/fake-environment-store.ts @@ -1,25 +1,20 @@ -import EnvironmentStore from '../../lib/db/environment-store'; import { IEnvironment } from '../../lib/types/model'; import NotFoundError from '../../lib/error/notfound-error'; -import noLoggerProvider from './no-logger'; +import { IEnvironmentStore } from '../../lib/types/stores/environment-store'; -export default class FakeEnvironmentStore extends EnvironmentStore { +export default class FakeEnvironmentStore implements IEnvironmentStore { environments: IEnvironment[] = []; - constructor() { - super(undefined, undefined, noLoggerProvider); - } - async getAll(): Promise { - return Promise.resolve(this.environments); + return this.environments; } - async exists(name: string): Promise { - return Promise.resolve(this.environments.some(e => e.name === name)); + async exists(name: string): Promise { + return this.environments.some((e) => e.name === name); } async getByName(name: string): Promise { - const env = this.environments.find(e => e.name === name); + const env = this.environments.find((e) => e.name === name); if (env) { return Promise.resolve(env); } @@ -29,43 +24,63 @@ export default class FakeEnvironmentStore extends EnvironmentStore { } async upsert(env: IEnvironment): Promise { - this.environments = this.environments.filter(e => e.name !== env.name); + this.environments = this.environments.filter( + (e) => e.name !== env.name, + ); this.environments.push(env); return Promise.resolve(env); } async connectProject( + // eslint-disable-next-line @typescript-eslint/no-unused-vars environment: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars projectId: string, ): Promise { return Promise.reject(new Error('Not implemented')); } async connectFeatures( + // eslint-disable-next-line @typescript-eslint/no-unused-vars environment: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars projectId: string, ): Promise { return Promise.reject(new Error('Not implemented')); } async delete(name: string): Promise { - this.environments = this.environments.filter(e => e.name !== name); + this.environments = this.environments.filter((e) => e.name !== name); return Promise.resolve(); } async disconnectProjectFromEnv( + // eslint-disable-next-line @typescript-eslint/no-unused-vars environment: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars projectId: string, ): Promise { return Promise.reject(new Error('Not implemented')); } async connectFeatureToEnvironmentsForProject( + // eslint-disable-next-line @typescript-eslint/no-unused-vars featureName: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars project_id: string, ): Promise { return Promise.reject(new Error('Not implemented')); } + + async deleteAll(): Promise { + this.environments = []; + } + + destroy(): void {} + + async get(key: string): Promise { + return this.environments.find((e) => e.name === key); + } } module.exports = FakeEnvironmentStore; diff --git a/src/test/fixtures/fake-event-store.ts b/src/test/fixtures/fake-event-store.ts index 2535ae300a..e55b47fec9 100644 --- a/src/test/fixtures/fake-event-store.ts +++ b/src/test/fixtures/fake-event-store.ts @@ -1,11 +1,12 @@ -import EventStore, { IEvent } from '../../lib/db/event-store'; -import noLoggerProvider from './no-logger'; +import EventEmitter from 'events'; +import { IEventStore } from '../../lib/types/stores/event-store'; +import { IEvent } from '../../lib/types/model'; -class FakeEventStore extends EventStore { +class FakeEventStore extends EventEmitter implements IEventStore { events: IEvent[]; constructor() { - super(undefined, noLoggerProvider); + super(); this.setMaxListeners(0); this.events = []; } @@ -17,15 +18,44 @@ class FakeEventStore extends EventStore { } batchStore(events: IEvent[]): Promise { - events.forEach(event => { + events.forEach((event) => { this.events.push(event); this.emit(event.type, event); }); return Promise.resolve(); } - getEvents(): Promise { - return Promise.resolve(this.events); + async getEvents(): Promise { + return this.events; + } + + async delete(key: number): Promise { + this.events.splice( + this.events.findIndex((t) => t.id === key), + 1, + ); + } + + async deleteAll(): Promise { + this.events = []; + } + + destroy(): void {} + + async exists(key: number): Promise { + return this.events.some((e) => e.id === key); + } + + async get(key: number): Promise { + return this.events.find((e) => e.id === key); + } + + async getAll(): Promise { + return this.events; + } + + async getEventsFilterByType(type: string): Promise { + return this.events.filter((e) => e.type === type); } } diff --git a/src/test/fixtures/fake-feature-environment-store.ts b/src/test/fixtures/fake-feature-environment-store.ts new file mode 100644 index 0000000000..baa56b5c87 --- /dev/null +++ b/src/test/fixtures/fake-feature-environment-store.ts @@ -0,0 +1,127 @@ +import { + FeatureEnvironmentKey, + IFeatureEnvironmentStore, +} from '../../lib/types/stores/feature-environment-store'; +import { IFeatureEnvironment } from '../../lib/types/model'; +import NotFoundError from '../../lib/error/notfound-error'; + +export default class FakeFeatureEnvironmentStore + implements IFeatureEnvironmentStore +{ + featureEnvironments: IFeatureEnvironment[] = []; + + async connectEnvironmentAndFeature( + featureName: string, + environment: string, + enabled: boolean, + ): Promise { + this.featureEnvironments.push({ environment, enabled, featureName }); + } + + async delete(key: FeatureEnvironmentKey): Promise { + this.featureEnvironments.splice( + this.featureEnvironments.findIndex( + (fE) => + fE.environment === key.environment && + fE.featureName === key.featureName, + ), + 1, + ); + } + + async deleteAll(): Promise { + this.featureEnvironments = []; + } + + destroy(): void {} + + async disconnectEnvironmentFromProject( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + environment: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + project: string, + ): Promise { + return Promise.resolve(undefined); + } + + async enableEnvironmentForFeature( + featureName: string, + environment: string, + ): Promise { + const fE = await this.get({ featureName, environment }); + fE.enabled = true; + } + + async exists(key: FeatureEnvironmentKey): Promise { + return this.featureEnvironments.some( + (fE) => + fE.featureName === key.featureName && + fE.environment === key.environment, + ); + } + + async featureHasEnvironment( + environment: string, + featureName: string, + ): Promise { + return this.exists({ environment, featureName }); + } + + async get(key: FeatureEnvironmentKey): Promise { + const featureEnvironment = this.featureEnvironments.find( + (fE) => + fE.environment === key.environment && + fE.featureName === key.featureName, + ); + if (featureEnvironment) { + return featureEnvironment; + } + throw new NotFoundError( + `Could not find environment ${key.environment} for feature: ${key.featureName}`, + ); + } + + async getAll(): Promise { + return this.featureEnvironments; + } + + async getAllFeatureEnvironments(): Promise { + return this.getAll(); + } + + getEnvironmentMetaData( + environment: string, + featureName: string, + ): Promise { + return this.get({ environment, featureName }); + } + + async isEnvironmentEnabled( + featureName: string, + environment: string, + ): Promise { + try { + const fE = await this.get({ featureName, environment }); + return fE.enabled; + } catch (e) { + return false; + } + } + + async removeEnvironmentForFeature( + featureName: string, + environment: string, + ): Promise { + return this.delete({ featureName, environment }); + } + + async toggleEnvironmentEnabledStatus( + environment: string, + featureName: string, + enabled: boolean, + ): Promise { + const fE = await this.get({ environment, featureName }); + fE.enabled = enabled; + return enabled; + } +} diff --git a/src/test/fixtures/fake-feature-strategies-store.ts b/src/test/fixtures/fake-feature-strategies-store.ts index 996b8953d2..c8de423c13 100644 --- a/src/test/fixtures/fake-feature-strategies-store.ts +++ b/src/test/fixtures/fake-feature-strategies-store.ts @@ -1,25 +1,23 @@ import { randomUUID } from 'crypto'; -import FeatureStrategiesStore, { - IFeatureStrategy, -} from '../../lib/db/feature-strategy-store'; -import noLoggerProvider from './no-logger'; import { FeatureToggle, - FeatureToggleDTO, FeatureToggleWithEnvironment, IFeatureEnvironment, - IFeatureOverview, + IFeatureStrategy, IFeatureToggleClient, IFeatureToggleQuery, } from '../../lib/types/model'; import NotFoundError from '../../lib/error/notfound-error'; +import { IFeatureStrategiesStore } from '../../lib/types/stores/feature-strategies-store'; interface ProjectEnvironment { projectName: string; environment: string; } -export default class FakeFeatureStrategiesStore extends FeatureStrategiesStore { +export default class FakeFeatureStrategiesStore + implements IFeatureStrategiesStore +{ environmentAndFeature: Map = new Map(); projectToEnvironment: ProjectEnvironment[] = []; @@ -28,10 +26,6 @@ export default class FakeFeatureStrategiesStore extends FeatureStrategiesStore { featureToggles: FeatureToggle[] = []; - constructor() { - super(undefined, undefined, noLoggerProvider); - } - async createStrategyConfig( strategyConfig: Omit, ): Promise { @@ -44,14 +38,16 @@ export default class FakeFeatureStrategiesStore extends FeatureStrategiesStore { featureName: string, ): Promise { return this.featureStrategies.filter( - fS => fS.featureName === featureName, + (fS) => fS.featureName === featureName, ); } + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types async createFeature(feature: any): Promise { this.featureToggles.push({ project: feature.project || 'default', createdAt: new Date(), + archived: false, ...feature, }); return Promise.resolve(); @@ -70,24 +66,51 @@ export default class FakeFeatureStrategiesStore extends FeatureStrategiesStore { environment: string, ): Promise { const stratEnvs = this.featureStrategies.filter( - fS => fS.environment === environment, + (fS) => fS.environment === environment, ); return Promise.resolve(stratEnvs); } + async hasStrategy(id: string): Promise { + return this.featureStrategies.some((s) => s.id === id); + } + + async get(id: string): Promise { + return this.featureStrategies.find((s) => s.id === id); + } + + async exists(key: string): Promise { + return this.featureStrategies.some((s) => s.id === key); + } + + async delete(key: string): Promise { + this.featureStrategies.splice( + this.featureStrategies.findIndex((s) => s.id === key), + 1, + ); + } + + async deleteAll(): Promise { + this.featureStrategies = []; + } + + destroy(): void { + throw new Error('Method not implemented.'); + } + async removeAllStrategiesForEnv( feature_name: string, environment: string, ): Promise { const toRemove = this.featureStrategies.filter( - fS => + (fS) => fS.featureName === feature_name && fS.environment === environment, ); this.featureStrategies = this.featureStrategies.filter( - f => + (f) => !toRemove.some( - r => + (r) => r.featureName === f.featureName && r.environment === f.environment, ), @@ -105,7 +128,7 @@ export default class FakeFeatureStrategiesStore extends FeatureStrategiesStore { environment: string, ): Promise { const rows = this.featureStrategies.filter( - fS => + (fS) => fS.projectName === project_name && fS.featureName === feature_name && fS.environment === environment, @@ -117,7 +140,7 @@ export default class FakeFeatureStrategiesStore extends FeatureStrategiesStore { environment: string, ): Promise { return this.featureStrategies.filter( - fS => fS.environment === environment, + (fS) => fS.environment === environment, ); } @@ -125,14 +148,14 @@ export default class FakeFeatureStrategiesStore extends FeatureStrategiesStore { featureName: string, archived: boolean = false, ): Promise { - const toggle = this.featureToggles.find(f => f.name === featureName); + const toggle = this.featureToggles.find( + (f) => f.name === featureName && f.archived === archived, + ); if (toggle) { - return Promise.resolve({ ...toggle, environments: [] }); + return { ...toggle, environments: [] }; } - return Promise.reject( - new NotFoundError( - `Could not find feature with name ${featureName}`, - ), + throw new NotFoundError( + `Could not find feature with name ${featureName}`, ); } @@ -140,7 +163,7 @@ export default class FakeFeatureStrategiesStore extends FeatureStrategiesStore { featureQuery?: IFeatureToggleQuery, archived: boolean = false, ): Promise { - const rows = this.featureToggles.filter(toggle => { + const rows = this.featureToggles.filter((toggle) => { if (featureQuery.namePrefix) { if (featureQuery.project) { return ( @@ -153,9 +176,9 @@ export default class FakeFeatureStrategiesStore extends FeatureStrategiesStore { if (featureQuery.project) { return featureQuery.project.includes(toggle.project); } - return true; + return toggle.archived === archived; }); - const clientRows: IFeatureToggleClient[] = rows.map(t => ({ + const clientRows: IFeatureToggleClient[] = rows.map((t) => ({ ...t, enabled: true, strategies: [], @@ -167,12 +190,8 @@ export default class FakeFeatureStrategiesStore extends FeatureStrategiesStore { return Promise.resolve(clientRows); } - async getProjectOverview(projectId: string): Promise { - return Promise.resolve([]); - } - async getStrategyById(id: string): Promise { - const strat = this.featureStrategies.find(fS => fS.id === id); + const strat = this.featureStrategies.find((fS) => fS.id === id); if (strat) { return Promise.resolve(strat); } @@ -207,13 +226,15 @@ export default class FakeFeatureStrategiesStore extends FeatureStrategiesStore { }, ]); } - const features = this.environmentAndFeature.get(environment).map(f => { - if (f.featureName === feature_name) { - // eslint-disable-next-line no-param-reassign - f.enabled = true; - } - return f; - }); + const features = this.environmentAndFeature + .get(environment) + .map((f) => { + if (f.featureName === feature_name) { + // eslint-disable-next-line no-param-reassign + f.enabled = true; + } + return f; + }); this.environmentAndFeature.set(environment, features); return Promise.resolve(); } @@ -226,7 +247,7 @@ export default class FakeFeatureStrategiesStore extends FeatureStrategiesStore { environment, this.environmentAndFeature .get(environment) - .filter(e => e.featureName !== feature_name), + .filter((e) => e.featureName !== feature_name), ); return Promise.resolve(); } @@ -236,7 +257,7 @@ export default class FakeFeatureStrategiesStore extends FeatureStrategiesStore { project: string, ): Promise { this.projectToEnvironment = this.projectToEnvironment.filter( - f => f.projectName !== project && f.environment !== environment, + (f) => f.projectName !== project && f.environment !== environment, ); return Promise.resolve(); } @@ -245,28 +266,28 @@ export default class FakeFeatureStrategiesStore extends FeatureStrategiesStore { id: string, updates: Partial, ): Promise { - this.featureStrategies = this.featureStrategies.map(f => { + this.featureStrategies = this.featureStrategies.map((f) => { if (f.id === id) { return { ...f, ...updates }; } return f; }); - return Promise.resolve(this.featureStrategies.find(f => f.id === id)); - } - - async getMembers(projectId: string): Promise { - return Promise.resolve(0); + return Promise.resolve(this.featureStrategies.find((f) => f.id === id)); } async getStrategiesAndMetadataForEnvironment( + // eslint-disable-next-line @typescript-eslint/no-unused-vars environment: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars featureName: string, ): Promise { return Promise.resolve(); } async deleteConfigurationsForProjectAndEnvironment( + // eslint-disable-next-line @typescript-eslint/no-unused-vars projectId: String, + // eslint-disable-next-line @typescript-eslint/no-unused-vars environment: String, ): Promise { return Promise.resolve(); @@ -279,7 +300,7 @@ export default class FakeFeatureStrategiesStore extends FeatureStrategiesStore { const enabled = this.environmentAndFeature .get(environment) - ?.find(f => f.featureName === featureName)?.enabled || false; + ?.find((f) => f.featureName === featureName)?.enabled || false; return Promise.resolve(enabled); } diff --git a/src/test/fixtures/fake-feature-tag-store.ts b/src/test/fixtures/fake-feature-tag-store.ts index fbb251a926..4157ad32bc 100644 --- a/src/test/fixtures/fake-feature-tag-store.ts +++ b/src/test/fixtures/fake-feature-tag-store.ts @@ -1,27 +1,44 @@ -import FeatureTagStore, { +import { ITag } from '../../lib/types/model'; +import { IFeatureAndTag, IFeatureTag, -} from '../../lib/db/feature-tag-store'; -import noLoggerProvider from './no-logger'; -import { ITag } from '../../lib/types/model'; + IFeatureTagStore, +} from '../../lib/types/stores/feature-tag-store'; -export default class FakeFeatureTagStore extends FeatureTagStore { +export default class FakeFeatureTagStore implements IFeatureTagStore { private featureTags: IFeatureTag[] = []; - constructor() { - super(undefined, undefined, noLoggerProvider); - } - async getAllTagsForFeature(featureName: string): Promise { const tags = this.featureTags - .filter(f => f.featureName === featureName) - .map(f => ({ + .filter((f) => f.featureName === featureName) + .map((f) => ({ type: f.tagType, value: f.tagValue, })); return Promise.resolve(tags); } + async delete(key: IFeatureTag): Promise { + this.featureTags.splice( + this.featureTags.findIndex((t) => t === key), + 1, + ); + } + + destroy(): void {} + + async exists(key: IFeatureTag): Promise { + return this.featureTags.some((t) => t === key); + } + + async get(key: IFeatureTag): Promise { + return this.featureTags.find((t) => t === key); + } + + async getAll(): Promise { + return this.featureTags; + } + async tagFeature(featureName: string, tag: ITag): Promise { this.featureTags.push({ featureName, @@ -35,7 +52,7 @@ export default class FakeFeatureTagStore extends FeatureTagStore { return Promise.resolve(this.featureTags); } - async dropFeatureTags(): Promise { + async deleteAll(): Promise { this.featureTags = []; return Promise.resolve(); } @@ -44,7 +61,7 @@ export default class FakeFeatureTagStore extends FeatureTagStore { featureTags: IFeatureTag[], ): Promise { return Promise.all( - featureTags.map(async fT => { + featureTags.map(async (fT) => { const saved = await this.tagFeature(fT.featureName, { value: fT.tagValue, type: fT.tagType, @@ -58,7 +75,7 @@ export default class FakeFeatureTagStore extends FeatureTagStore { } async untagFeature(featureName: string, tag: ITag): Promise { - this.featureTags = this.featureTags.filter(fT => { + this.featureTags = this.featureTags.filter((fT) => { if (fT.featureName === featureName) { return !(fT.tagType === tag.type && fT.tagValue === tag.value); } diff --git a/src/test/fixtures/fake-feature-toggle-store.js b/src/test/fixtures/fake-feature-toggle-store.js deleted file mode 100644 index c4b57ee9b9..0000000000 --- a/src/test/fixtures/fake-feature-toggle-store.js +++ /dev/null @@ -1,146 +0,0 @@ -'use strict'; - -module.exports = (databaseIsUp = true) => { - const _features = []; - const _archive = []; - const _featureTags = []; - - return { - getFeature: name => { - if (!databaseIsUp) { - return Promise.reject(new Error('No database connection')); - } - const toggle = _features.find(f => f.name === name); - if (toggle) { - return Promise.resolve(toggle); - } - return Promise.reject(new Error('could not find toggle')); - }, - hasFeature: name => { - const toggle = _features.find(f => f.name === name); - const archived = _archive.find(f => f.name === name); - if (toggle) { - return Promise.resolve({ name, archived: false }); - } - if (archived) { - return Promise.resolve({ name, archived: true }); - } - return Promise.reject(); - }, - updateFeature: updatedFeature => { - _features.splice( - _features.indexOf(f => f.name === updatedFeature.name), - 1, - ); - _features.push(updatedFeature); - }, - archiveFeature: feature => { - _features.slice( - _features.indexOf(({ name }) => name === feature.name), - 1, - ); - _archive.push(feature); - }, - createFeature: async (project, feature) => { - _features.push(feature); - return Promise.resolve(feature); - }, - getArchivedFeatures: () => Promise.resolve(_archive), - addArchivedFeature: feature => _archive.push(feature), - reviveFeature: feature => { - const revived = _archive.find(f => f.name === feature.name); - _archive.splice( - _archive.indexOf(f => f.name === feature.name), - 1, - ); - _features.push(revived); - }, - lastSeenToggles: (names = []) => { - names.forEach(name => { - const toggle = _features.find(f => f.name === name); - if (toggle) { - toggle.lastSeenAt = new Date(); - } - }); - }, - dropFeatures: () => { - _features.splice(0, _features.length); - _archive.splice(0, _archive.length); - }, - deleteFeature: featureName => { - const archivedIdx = _archive.findIndex(f => f.name === featureName); - if (archivedIdx > -1) { - _archive.splice(archivedIdx, 1); - } - return Promise.resolve(); - }, - importFeature: feat => Promise.resolve(_features.push(feat)), - getFeatures: query => { - if (!databaseIsUp) { - return Promise.reject(new Error('No database connection')); - } - if (query) { - const activeQueryKeys = Object.keys(query).filter( - t => query[t], - ); - const filtered = _features.filter(feature => - activeQueryKeys.every(key => { - if (key === 'namePrefix') { - return feature.name.indexOf(query[key]) > -1; - } - if (key === 'tag') { - return query[key].some(tagQuery => - _featureTags - .filter(t => t.featureName === feature.name) - .some( - tag => - tag.tagType === tagQuery[0] && - tag.tagValue === tagQuery[1], - ), - ); - } - return query[key].some(v => v === feature[key]); - }), - ); - return Promise.resolve(filtered); - } - return Promise.resolve(_features); - }, - tagFeature: (featureName, tag) => { - _featureTags.push({ - featureName, - tagType: tag.type, - tagValue: tag.value, - }); - }, - untagFeature: event => { - const index = _featureTags.findIndex( - f => - f.featureName === event.featureName && - f.tagType === event.type && - f.tagValue === event.value, - ); - _featureTags.splice(index, 1); - }, - getAllTagsForFeature: featureName => - Promise.resolve( - _featureTags - .filter(f => f.featureName === featureName) - .map(t => ({ - type: t.tagType, - value: t.tagValue, - })), - ), - getAllFeatureTags: () => Promise.resolve(_featureTags), - importFeatureTags: tags => { - tags.forEach(tag => { - _featureTags.push(tag); - }); - return Promise.resolve(_featureTags); - }, - dropFeatureTags: () => { - _featureTags.splice(0, _featureTags.length); - return Promise.resolve(); - }, - }; -}; diff --git a/src/test/fixtures/fake-feature-toggle-store.ts b/src/test/fixtures/fake-feature-toggle-store.ts new file mode 100644 index 0000000000..dff486809b --- /dev/null +++ b/src/test/fixtures/fake-feature-toggle-store.ts @@ -0,0 +1,138 @@ +import { + IFeatureToggleQuery, + IFeatureToggleStore, + IHasFeature, +} from '../../lib/types/stores/feature-toggle-store'; +import { FeatureToggle, FeatureToggleDTO } from '../../lib/types/model'; +import NotFoundError from '../../lib/error/notfound-error'; + +export default class FakeFeatureToggleStore implements IFeatureToggleStore { + features: FeatureToggle[] = []; + + async archiveFeature(featureName: string): Promise { + const feature = this.features.find((f) => f.name === featureName); + if (feature) { + feature.archived = true; + return feature; + } + throw new NotFoundError( + `Could not find feature toggle with name ${featureName}`, + ); + } + + async count(query: Partial): Promise { + return this.features.filter(this.getFilterQuery(query)).length; + } + + async getProjectId(name: string): Promise { + return this.get(name).then((f) => f.project); + } + + private getFilterQuery(query: Partial) { + return (f) => { + let projectMatch = true; + if (query.project) { + projectMatch = f.project === query.project; + } + let archiveMatch = true; + if (query.archived) { + archiveMatch = f.archived === query.archived; + } + let staleMatch = true; + if (query.stale) { + staleMatch = f.stale === query.stale; + } + return projectMatch && archiveMatch && staleMatch; + }; + } + + async createFeature( + project: string, + data: FeatureToggleDTO, + ): Promise { + const inserted: FeatureToggle = { ...data, project }; + this.features.push(inserted); + return inserted; + } + + async delete(key: string): Promise { + this.features.splice( + this.features.findIndex((f) => f.name === key), + 1, + ); + } + + async deleteAll(): Promise { + this.features = []; + } + + destroy(): void {} + + async exists(key: string): Promise { + return this.features.some((f) => f.name === key); + } + + async get(key: string): Promise { + const feature = this.features.find((f) => f.name === key); + if (feature) { + return feature; + } + throw new NotFoundError(`Could not find feature with name ${key}`); + } + + async getAll(): Promise { + return this.features.filter((f) => !f.archived); + } + + async getFeatureMetadata(name: string): Promise { + return this.get(name); + } + + async getFeatures(archived: boolean): Promise { + return this.features.filter((f) => f.archived === archived); + } + + async getFeaturesBy( + query: Partial, + ): Promise { + return this.features.filter(this.getFilterQuery(query)); + } + + async hasFeature(featureName: string): Promise { + const { name, archived } = await this.get(featureName); + return { name, archived }; + } + + async reviveFeature(featureName: string): Promise { + const revive = this.features.find((f) => f.name === featureName); + if (revive) { + revive.archived = false; + } + return this.updateFeature(revive.project, revive); + } + + async updateFeature( + project: string, + data: FeatureToggleDTO, + ): Promise { + const exists = await this.exists(data.name); + if (exists) { + const id = this.features.findIndex((f) => f.name === data.name); + const old = this.features.find((f) => f.name === data.name); + const updated = { ...old, ...data }; + this.features.splice(id, 1); + this.features.push(updated); + return updated; + } + throw new NotFoundError('Could not find feature to update'); + } + + async updateLastSeenForToggles(toggleNames: string[]): Promise { + toggleNames.forEach((t) => { + const toUpdate = this.features.find((f) => f.name === t); + if (toUpdate) { + toUpdate.lastSeenAt = new Date(); + } + }); + } +} diff --git a/src/test/fixtures/fake-feature-type-store.ts b/src/test/fixtures/fake-feature-type-store.ts new file mode 100644 index 0000000000..d9c95ac175 --- /dev/null +++ b/src/test/fixtures/fake-feature-type-store.ts @@ -0,0 +1,48 @@ +import { + IFeatureType, + IFeatureTypeStore, +} from '../../lib/types/stores/feature-type-store'; +import NotFoundError from '../../lib/error/notfound-error'; + +export default class FakeFeatureTypeStore implements IFeatureTypeStore { + featureTypes: IFeatureType[] = []; + + async delete(key: number): Promise { + this.featureTypes.splice( + this.featureTypes.findIndex((type) => type.id === key), + 1, + ); + } + + async deleteAll(): Promise { + this.featureTypes = []; + } + + destroy(): void {} + + async exists(key: number): Promise { + return this.featureTypes.some((fT) => fT.id === key); + } + + async get(key: number): Promise { + const type = this.featureTypes.find((fT) => fT.id === key); + if (type) { + return type; + } + throw new NotFoundError(`Could not find feature type with id : ${key}`); + } + + async getAll(): Promise { + return this.featureTypes; + } + + async getByName(name: string): Promise { + const type = this.featureTypes.find((fT) => fT.name === name); + if (type) { + return type; + } + throw new NotFoundError( + `Could not find feature type with name: ${name}`, + ); + } +} diff --git a/src/test/fixtures/fake-metrics-store.js b/src/test/fixtures/fake-metrics-store.js deleted file mode 100644 index cc44055452..0000000000 --- a/src/test/fixtures/fake-metrics-store.js +++ /dev/null @@ -1,20 +0,0 @@ -'use strict'; - -const { EventEmitter } = require('events'); - -class FakeMetricsStore extends EventEmitter { - constructor() { - super(); - this.setMaxListeners(0); - } - - getMetricsLastHour() { - return Promise.resolve([]); - } - - insert() { - return Promise.resolve(); - } -} - -module.exports = FakeMetricsStore; diff --git a/src/test/fixtures/fake-project-store.js b/src/test/fixtures/fake-project-store.js deleted file mode 100644 index 7fd8e94be6..0000000000 --- a/src/test/fixtures/fake-project-store.js +++ /dev/null @@ -1,35 +0,0 @@ -const NotFoundError = require('../../lib/error/notfound-error'); - -module.exports = (databaseIsUp = true) => { - const _projects = []; - return { - create: project => { - _projects.push(project); - return Promise.resolve(); - }, - getAll: () => { - if (databaseIsUp) { - return Promise.resolve(_projects); - } - return Promise.reject(new Error('Database is down')); - }, - importProjects: projects => { - projects.forEach(project => { - _projects.push(project); - }); - return Promise.resolve(_projects); - }, - dropProjects: () => { - _projects.splice(0, _projects.length); - }, - hasProject: id => { - const project = _projects.find(p => p.id === id); - if (project) { - return Promise.resolve(project); - } - return Promise.reject( - new NotFoundError(`Could not find project with id ${id}`), - ); - }, - }; -}; diff --git a/src/test/fixtures/fake-project-store.ts b/src/test/fixtures/fake-project-store.ts new file mode 100644 index 0000000000..bda4789324 --- /dev/null +++ b/src/test/fixtures/fake-project-store.ts @@ -0,0 +1,115 @@ +import { + IProject, + IProjectHealthUpdate, + IProjectInsert, + IProjectStore, +} from '../../lib/types/stores/project-store'; +import { IFeatureOverview } from '../../lib/types/model'; +import NotFoundError from '../../lib/error/notfound-error'; + +export default class FakeProjectStore implements IProjectStore { + getEnvironmentsForProject(): Promise { + throw new Error('Method not implemented.'); + } + + projects: IProject[] = []; + + projectEnvironment: Map> = new Map(); + + async addEnvironmentToProject( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + id: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + environment: string, + ): Promise { + const environments = this.projectEnvironment.get(id) || new Set(); + environments.add(environment); + this.projectEnvironment.set(id, environments); + } + + private createInternal(project: IProjectInsert): IProject { + const newProj: IProject = { + ...project, + health: 100, + createdAt: new Date(), + }; + this.projects.push(newProj); + return newProj; + } + + async create(project: IProjectInsert): Promise { + return this.createInternal(project); + } + + async delete(key: string): Promise { + this.projects.splice( + this.projects.findIndex((p) => p.id === key), + 1, + ); + } + + async deleteAll(): Promise { + this.projects = []; + } + + async deleteEnvironmentForProject( + id: string, + environment: string, + ): Promise { + const environments = this.projectEnvironment.get(id); + if (environments) { + environments.delete(environment); + this.projectEnvironment.set(id, environments); + } + } + + destroy(): void {} + + async exists(key: string): Promise { + return this.projects.some((p) => p.id === key); + } + + async get(key: string): Promise { + const project = this.projects.find((p) => p.id === key); + if (project) { + return project; + } + throw new NotFoundError(`Could not find project with id: ${key}`); + } + + async getAll(): Promise { + return this.projects; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async getMembers(projectId: string): Promise { + return Promise.resolve(0); + } + + async getProjectOverview( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + projectId: string, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + archived: boolean, + ): Promise { + return Promise.resolve([]); + } + + async hasProject(id: string): Promise { + return this.exists(id); + } + + async importProjects(projects: IProjectInsert[]): Promise { + return projects.map((p) => this.createInternal(p)); + } + + async update(update: IProjectInsert): Promise { + await this.delete(update.id); + this.createInternal(update); + } + + async updateHealth(healthUpdate: IProjectHealthUpdate): Promise { + this.projects.find((p) => p.id === healthUpdate.id).health = + healthUpdate.health; + } +} diff --git a/src/test/fixtures/fake-reset-token-store.ts b/src/test/fixtures/fake-reset-token-store.ts index ceef32f65c..a87d54ddf5 100644 --- a/src/test/fixtures/fake-reset-token-store.ts +++ b/src/test/fixtures/fake-reset-token-store.ts @@ -1,23 +1,21 @@ -import { EventEmitter } from 'events'; +import NotFoundError from '../../lib/error/notfound-error'; import { + IResetQuery, IResetToken, IResetTokenCreate, IResetTokenQuery, - ResetTokenStore, -} from '../../lib/db/reset-token-store'; -import noLoggerProvider from './no-logger'; -import NotFoundError from '../../lib/error/notfound-error'; + IResetTokenStore, +} from '../../lib/types/stores/reset-token-store'; -export class ResetTokenStoreMock extends ResetTokenStore { +export default class FakeResetTokenStore implements IResetTokenStore { data: IResetToken[]; constructor() { - super(undefined, new EventEmitter(), noLoggerProvider); this.data = []; } async getActive(token: string): Promise { - const row = this.data.find(tokens => tokens.token === token); + const row = this.data.find((tokens) => tokens.token === token); if (!row) { throw new NotFoundError(); } @@ -36,9 +34,9 @@ export class ResetTokenStoreMock extends ResetTokenStore { return Promise.resolve(token); } - async delete({ reset_token }: IResetTokenQuery): Promise { + async delete(token: string): Promise { this.data.splice( - this.data.findIndex(token => token.token === reset_token), + this.data.findIndex((t) => t.token === token), 1, ); return Promise.resolve(); @@ -47,4 +45,53 @@ export class ResetTokenStoreMock extends ResetTokenStore { async deleteExpired(): Promise { throw new Error('Not implemented in mock'); } + + async deleteAll(): Promise { + this.data = []; + } + + async deleteFromQuery(query: IResetTokenQuery): Promise { + this.data = this.data.filter( + (t) => t.userId !== query.user_id && t.token !== query.reset_token, + ); + } + + destroy(): void {} + + async exists(token: string): Promise { + return this.data.some((f) => f.token === token); + } + + async expireExistingTokensForUser(user_id: number): Promise { + this.data + .filter((f) => f.userId === user_id) + .forEach((t) => { + // eslint-disable-next-line no-param-reassign + t.expiresAt = new Date(); + }); + } + + async get(token: string): Promise { + return this.data.find((t) => t.token === token); + } + + async getActiveTokens(): Promise { + const now = new Date(); + return this.data.filter((t) => t.expiresAt > now); + } + + async getAll(): Promise { + return this.data; + } + + async useToken(token: IResetQuery): Promise { + if (this.exists(token.token)) { + const d = this.data.find( + (t) => t.usedAt === null && t.token === token.token, + ); + d.usedAt = new Date(); + return true; + } + return false; + } } diff --git a/src/test/fixtures/fake-session-store.ts b/src/test/fixtures/fake-session-store.ts index 79abf56142..fc27777ccf 100644 --- a/src/test/fixtures/fake-session-store.ts +++ b/src/test/fixtures/fake-session-store.ts @@ -1,24 +1,52 @@ -import SessionStore, { ISession } from '../../lib/db/session-store'; -import noLoggerProvider from './no-logger'; +import { ISession, ISessionStore } from '../../lib/types/stores/session-store'; -export default class FakeSessionStore extends SessionStore { +export default class FakeSessionStore implements ISessionStore { private sessions: ISession[] = []; - constructor() { - super(undefined, undefined, noLoggerProvider); + async getActiveSessions(): Promise { + return this.sessions.filter((session) => session.expired != null); } - async getActiveSessions(): Promise { - return this.sessions.filter(session => session.expired != null); + destroy(): void {} + + async exists(key: string): Promise { + return this.sessions.some((s) => s.sid === key); + } + + async getAll(): Promise { + return this.sessions; } async getSessionsForUser(userId: number): Promise { - return this.sessions.filter(session => session.sess.user.id === userId); + return this.sessions.filter( + (session) => session.sess.user.id === userId, + ); } async deleteSessionsForUser(userId: number): Promise { this.sessions = this.sessions.filter( - session => session.sess.user.id !== userId, + (session) => session.sess.user.id !== userId, ); } + + async deleteAll(): Promise { + this.sessions = []; + } + + async delete(sid: string): Promise { + this.sessions.splice( + this.sessions.findIndex((s) => s.sid === sid), + 1, + ); + } + + async get(sid: string): Promise { + return this.sessions.find((s) => s.sid === sid); + } + + async insertSession(data: Omit): Promise { + const session = { ...data, createdAt: new Date() }; + this.sessions.push(session); + return session; + } } diff --git a/src/test/fixtures/fake-setting-store.js b/src/test/fixtures/fake-setting-store.js deleted file mode 100644 index 07c262e091..0000000000 --- a/src/test/fixtures/fake-setting-store.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; - -module.exports = () => { - const _settings = []; - return { - insert: setting => { - _settings.push(setting); - return Promise.resolve(); - }, - get: name => { - const setting = _settings.find(s => s.name === name); - if (setting) { - return Promise.resolve(setting.content); - } - return Promise.reject(new Error('Could not find setting')); - }, - }; -}; diff --git a/src/test/fixtures/fake-setting-store.ts b/src/test/fixtures/fake-setting-store.ts new file mode 100644 index 0000000000..fb0f0d9d9d --- /dev/null +++ b/src/test/fixtures/fake-setting-store.ts @@ -0,0 +1,42 @@ +import { ISettingStore } from '../../lib/types/stores/settings-store'; +import NotFoundError from '../../lib/error/notfound-error'; + +export default class FakeSettingStore implements ISettingStore { + settings: Map = new Map(); + + async delete(key: string): Promise { + this.settings.delete(key); + } + + async deleteAll(): Promise { + this.settings = new Map(); + } + + destroy(): void {} + + async exists(key: string): Promise { + return this.settings.has(key); + } + + async get(key: string): Promise { + const setting = this.settings.get(key); + if (setting) { + return setting; + } + throw new NotFoundError(`Could not find setting with key ${key}`); + } + + async getAll(): Promise { + return Array.from(this.settings.values()); + } + + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + async insert(name: string, content: any): Promise { + this.settings.set(name, content); + } + + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + async updateRow(name: string, content: any): Promise { + this.settings.set(name, content); + } +} diff --git a/src/test/fixtures/fake-strategies-store.js b/src/test/fixtures/fake-strategies-store.js deleted file mode 100644 index a274af8454..0000000000 --- a/src/test/fixtures/fake-strategies-store.js +++ /dev/null @@ -1,60 +0,0 @@ -'use strict'; - -const NotFoundError = require('../../lib/error/notfound-error'); - -module.exports = (databaseIsUp = true) => { - const _strategies = [ - { name: 'default', editable: false, parameters: {}, deprecated: false }, - ]; - - return { - getStrategies: () => { - if (databaseIsUp) { - return Promise.resolve(_strategies); - } - return Promise.reject(new Error('No database connection')); - }, - getEditableStrategies: () => - Promise.resolve(_strategies.filter(s => s.editable)), - getStrategy: name => { - const strategy = _strategies.find(s => s.name === name); - if (strategy) { - return Promise.resolve(strategy); - } - return Promise.reject(new NotFoundError('Not found!')); - }, - createStrategy: strat => _strategies.push(strat), - updateStrategy: strat => { - _strategies.splice( - _strategies.indexOf(({ name }) => name === strat.name), - 1, - ); - _strategies.push(strat); - }, - importStrategy: strat => Promise.resolve(_strategies.push(strat)), - dropStrategies: () => _strategies.splice(0, _strategies.length), - deleteStrategy: strat => - _strategies.splice( - _strategies.indexOf(({ name }) => name === strat.name), - 1, - ), - deprecateStrategy: ({ name }) => { - const deprecatedStrat = _strategies.find(s => s.name === name); - deprecatedStrat.deprecated = true; - _strategies.splice( - _strategies.indexOf(s => name === s.name), - 1, - ); - _strategies.push(deprecatedStrat); - }, - reactivateStrategy: ({ name }) => { - const reactivatedStrat = _strategies.find(s => s.name === name); - reactivatedStrat.deprecated = false; - _strategies.splice( - _strategies.indexOf(s => name === s.name), - 1, - ); - _strategies.push(reactivatedStrat); - }, - }; -}; diff --git a/src/test/fixtures/fake-strategies-store.ts b/src/test/fixtures/fake-strategies-store.ts new file mode 100644 index 0000000000..24374d7abf --- /dev/null +++ b/src/test/fixtures/fake-strategies-store.ts @@ -0,0 +1,122 @@ +import { + IEditableStrategy, + IMinimalStrategy, + IStrategy, + IStrategyStore, +} from '../../lib/types/stores/strategy-store'; +import NotFoundError from '../../lib/error/notfound-error'; + +export default class FakeStrategiesStore implements IStrategyStore { + defaultStrategy: IStrategy = { + name: 'default', + description: 'default strategy', + displayName: 'Default', + editable: false, + parameters: {}, + deprecated: false, + }; + + strategies: IStrategy[] = [this.defaultStrategy]; + + async createStrategy(update: IMinimalStrategy): Promise { + let params; + if ( + typeof update.parameters === 'string' || + typeof update.parameters === 'number' + ) { + if (update.parameters === '') { + params = {}; + } else { + params = JSON.parse(update.parameters); + } + } else { + params = update.parameters; + } + this.strategies.push({ + editable: true, + deprecated: false, + description: '', + displayName: update.name, + ...update, + parameters: params, + }); + } + + async delete(key: string): Promise { + this.strategies.splice( + this.strategies.findIndex((k) => k.name === key), + 1, + ); + } + + async deleteAll(): Promise { + this.strategies = [this.defaultStrategy]; + } + + async deleteStrategy({ name }: Pick): Promise { + return this.delete(name); + } + + async deprecateStrategy({ name }: Pick): Promise { + const strategy = await this.get(name); + strategy.deprecated = true; + } + + destroy(): void {} + + async dropStrategies(): Promise { + return this.deleteAll(); + } + + async exists(key: string): Promise { + return this.strategies.some((s) => s.name === key && !s.deprecated); + } + + async get(key: string): Promise { + const strat = this.strategies.find((s) => s.name === key); + if (strat) { + return strat; + } + throw new NotFoundError(`Could not find strategy with name: ${key}`); + } + + async getAll(): Promise { + return this.strategies.filter((s) => !s.deprecated); + } + + async getEditableStrategies(): Promise { + return this.strategies + .filter((s) => s.editable) + .map((s) => { + if (!s.parameters) { + // eslint-disable-next-line no-param-reassign + s.parameters = []; + } + return s; + }); + } + + async getStrategy(name: string): Promise { + const strat = this.get(name); + if (strat) { + return strat; + } + throw new NotFoundError(`Could not find strategy with name: ${name}`); + } + + async importStrategy(data: IMinimalStrategy): Promise { + return this.createStrategy(data); + } + + async reactivateStrategy({ name }: Pick): Promise { + const strategy = await this.get(name); + strategy.deprecated = false; + await this.delete(name); + this.strategies.push(strategy); + } + + async updateStrategy(update: IMinimalStrategy): Promise { + await this.delete(update.name); + return this.createStrategy(update); + } +} diff --git a/src/test/fixtures/fake-tag-store.js b/src/test/fixtures/fake-tag-store.js deleted file mode 100644 index c404af1b90..0000000000 --- a/src/test/fixtures/fake-tag-store.js +++ /dev/null @@ -1,50 +0,0 @@ -const NotFoundError = require('../../lib/error/notfound-error'); - -module.exports = (databaseIsUp = true) => { - const _tags = []; - return { - getTagsByType: type => { - if (!databaseIsUp) { - return Promise.reject(new Error('No database connection')); - } - const tags = _tags.filter(t => t.type === type); - return Promise.resolve(tags); - }, - createTag: tag => { - _tags.push({ value: tag.value, type: tag.type }); - }, - deleteTag: tag => { - _tags.splice( - _tags.indexOf( - t => t.value === tag.value && t.type === tag.type, - ), - 1, - ); - }, - getAll: () => { - if (!databaseIsUp) { - return Promise.reject(new Error('No database connection')); - } - return Promise.resolve(_tags); - }, - getTag: (type, value) => { - const tag = _tags.find(t => t.type === type && t.value === value); - if (tag) { - return Promise.resolve(tag); - } - return Promise.reject(new NotFoundError('Could not find tag')); - }, - bulkImport: tags => { - tags.forEach(tag => _tags.push(tag)); - return Promise.resolve(_tags); - }, - dropTags: () => { - _tags.splice(0, _tags.length); - return Promise.resolve(); - }, - exists: tag => - Promise.resolve( - _tags.some(t => t.type === tag.type && t.value === tag.value), - ), - }; -}; diff --git a/src/test/fixtures/fake-tag-store.ts b/src/test/fixtures/fake-tag-store.ts new file mode 100644 index 0000000000..30e55dd0f6 --- /dev/null +++ b/src/test/fixtures/fake-tag-store.ts @@ -0,0 +1,54 @@ +import { ITagStore } from '../../lib/types/stores/tag-store'; +import { ITag } from '../../lib/types/model'; +import NotFoundError from '../../lib/error/notfound-error'; + +export default class FakeTagStore implements ITagStore { + tags: ITag[] = []; + + async bulkImport(tags: ITag[]): Promise { + tags.forEach((t) => this.tags.push(t)); + return tags; + } + + async createTag(tag: ITag): Promise { + this.tags.push(tag); + } + + async delete(key: ITag): Promise { + this.tags.splice(this.tags.findIndex((t) => t === key)); + } + + async deleteAll(): Promise { + this.tags = []; + } + + destroy(): void {} + + async exists(key: ITag): Promise { + return this.tags.some((t) => t === key); + } + + async get(key: ITag): Promise { + const tag = this.tags.find((t) => t === key); + if (tag) { + return tag; + } + throw new NotFoundError('Tag does not exist'); + } + + async getAll(): Promise { + return this.tags; + } + + async getTag(type: string, value: string): Promise { + const tag = this.tags.find((t) => t.type === type && t.value === value); + if (tag) { + return tag; + } + throw new NotFoundError('Tag does not exist'); + } + + async getTagsByType(type: string): Promise { + return this.tags.filter((t) => t.type === type); + } +} diff --git a/src/test/fixtures/fake-tag-type-store.js b/src/test/fixtures/fake-tag-type-store.js deleted file mode 100644 index aad24ac87e..0000000000 --- a/src/test/fixtures/fake-tag-type-store.js +++ /dev/null @@ -1,27 +0,0 @@ -const NotFoundError = require('../../lib/error/notfound-error'); - -module.exports = () => { - const _tagTypes = []; - return { - getTagType: async name => { - const tag = _tagTypes.find(t => t.name === name); - if (tag) { - return Promise.resolve(tag); - } - return Promise.reject(new NotFoundError('Could not find tag type')); - }, - createTagType: async tag => { - _tagTypes.push(tag); - }, - getAll: () => Promise.resolve(_tagTypes), - bulkImport: tagTypes => { - tagTypes.forEach(tagType => _tagTypes.push(tagType)); - return Promise.resolve(_tagTypes); - }, - dropTagTypes: () => { - _tagTypes.splice(0, _tagTypes.length); - return Promise.resolve(); - }, - exists: name => Promise.resolve(_tagTypes.some(t => t.name === name)), - }; -}; diff --git a/src/test/fixtures/fake-tag-type-store.ts b/src/test/fixtures/fake-tag-type-store.ts new file mode 100644 index 0000000000..3935fa2cb7 --- /dev/null +++ b/src/test/fixtures/fake-tag-type-store.ts @@ -0,0 +1,50 @@ +import { ITagType, ITagTypeStore } from '../../lib/types/stores/tag-type-store'; + +const NotFoundError = require('../../lib/error/notfound-error'); + +export default class FakeTagTypeStore implements ITagTypeStore { + tagTypes: ITagType[] = []; + + async bulkImport(tagTypes: ITagType[]): Promise { + tagTypes.forEach((tT) => this.tagTypes.push(tT)); + return tagTypes; + } + + async createTagType(newTagType: ITagType): Promise { + this.tagTypes.push(newTagType); + } + + async delete(key: string): Promise { + this.tagTypes.splice( + this.tagTypes.findIndex((tt) => tt.name === key), + 1, + ); + } + + async deleteAll(): Promise { + this.tagTypes = []; + } + + destroy(): void {} + + async exists(key: string): Promise { + return this.tagTypes.some((t) => t.name === key); + } + + async get(key: string): Promise { + const tagType = this.tagTypes.find((t) => t.name === key); + if (tagType) { + return tagType; + } + throw new NotFoundError('Could not find tag type'); + } + + async getAll(): Promise { + return this.tagTypes; + } + + async updateTagType(tagType: ITagType): Promise { + await this.delete(tagType.name); + return this.createTagType(tagType); + } +} diff --git a/src/test/fixtures/fake-user-feedback-store.ts b/src/test/fixtures/fake-user-feedback-store.ts index f86cf03e46..afdeccd98d 100644 --- a/src/test/fixtures/fake-user-feedback-store.ts +++ b/src/test/fixtures/fake-user-feedback-store.ts @@ -1,5 +1,47 @@ -module.exports = () => ({ - getAllUserFeedback: () => Promise.resolve([]), - getFeedback: () => Promise.resolve({}), - updateFeedback: () => Promise.resolve({}), -}); +import { + IUserFeedback, + IUserFeedbackKey, + IUserFeedbackStore, +} from '../../lib/types/stores/user-feedback-store'; + +export default class FakeUserFeedbackStore implements IUserFeedbackStore { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + delete(key: IUserFeedbackKey): Promise { + return Promise.resolve(undefined); + } + + deleteAll(): Promise { + return Promise.resolve(undefined); + } + + destroy(): void {} + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + exists(key: IUserFeedbackKey): Promise { + return Promise.resolve(false); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + get(key: IUserFeedbackKey): Promise { + return Promise.resolve(undefined); + } + + getAll(): Promise { + return Promise.resolve([]); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getAllUserFeedback(userId: number): Promise { + return Promise.resolve([]); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getFeedback(userId: number, feedbackId: string): Promise { + return Promise.resolve(undefined); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + updateFeedback(feedback: IUserFeedback): Promise { + return Promise.resolve(undefined); + } +} diff --git a/src/test/fixtures/fake-user-store.ts b/src/test/fixtures/fake-user-store.ts index 911396d3e0..66bc97d2f4 100644 --- a/src/test/fixtures/fake-user-store.ts +++ b/src/test/fixtures/fake-user-store.ts @@ -1,14 +1,16 @@ -import UserStore, { IUserLookup } from '../../lib/db/user-store'; -import User from '../../lib/types/user'; -import noLoggerProvider from './no-logger'; +import User, { IUser } from '../../lib/types/user'; +import { + ICreateUser, + IUserLookup, + IUserStore, +} from '../../lib/types/stores/user-store'; -class UserStoreMock extends UserStore { - data: any[]; +class UserStoreMock implements IUserStore { + data: IUser[]; idSeq: number; constructor() { - super(undefined, noLoggerProvider); this.idSeq = 1; this.data = []; } @@ -18,13 +20,26 @@ class UserStoreMock extends UserStore { username, email, }: IUserLookup): Promise { - const user = this.data.find(i => { + const user = this.data.find((i) => { if (id && i.id === id) return true; if (username && i.username === username) return true; if (email && i.email === email) return true; return false; }); - return user; + if (user) { + return user.id; + } + return undefined; + } + + destroy(): void {} + + async exists(key: number): Promise { + return this.data.some((u) => u.id === key); + } + + async get(key: number): Promise { + return this.data.find((u) => u.id === key); } async insert(user: User): Promise { @@ -37,15 +52,15 @@ class UserStoreMock extends UserStore { async update(id: number, user: User): Promise { // eslint-disable-next-line no-param-reassign - this.data = this.data.map(o => { + this.data = this.data.map((o) => { if (o.id === id) return { ...o, name: user.name }; return o; }); return Promise.resolve(user); } - async get({ id, username, email }: IUserLookup): Promise { - const user = this.data.find(i => { + async getByQuery({ id, username, email }: IUserLookup): Promise { + const user = this.data.find((i) => { if (i.id && i.id === id) return true; if (i.username && i.username === username) return true; if (i.email && i.email === email) return true; @@ -57,48 +72,67 @@ class UserStoreMock extends UserStore { throw new Error('Could not find user'); } - async getAll(): Promise { + async getAll(): Promise { return Promise.resolve(this.data); } async setPasswordHash(userId: number, passwordHash: string): Promise { - const u = this.data.find(a => a.id === userId); + const u = this.data.find((a) => a.id === userId); + // @ts-ignore u.passwordHash = passwordHash; return Promise.resolve(); } async getPasswordHash(id: number): Promise { - const u = this.data.find(i => i.id === id); + const u = this.data.find((i) => i.id === id); + // @ts-ignore return Promise.resolve(u.passwordHash); } async delete(id: number): Promise { - this.data = this.data.filter(item => item.id !== id); + this.data = this.data.filter((item) => item.id !== id); return Promise.resolve(); } async successfullyLogin(user: User): Promise { - const u = this.data.find(i => i.id === user.id); - u.login_attempts = 0; - u.seen_at = new Date(); - return Promise.resolve(); + if (!this.exists(user.id)) { + throw new Error('No such user'); + } } buildSelectUser(): any { throw new Error('Not implemented'); } - async search(): Promise { + async search(): Promise { throw new Error('Not implemented'); } - async getAllWithId(): Promise { + async getAllWithId(): Promise { throw new Error('Not implemented'); } async incLoginAttempts(): Promise { throw new Error('Not implemented'); } + + deleteAll(): Promise { + return Promise.resolve(undefined); + } + + upsert(user: ICreateUser): Promise { + this.data.splice(this.data.findIndex((u) => u.email === user.email)); + this.data.push({ + id: this.data.length + 1, + createdAt: new Date(), + isAPI: false, + permissions: [], + loginAttempts: 0, + imageUrl: '', + ...user, + }); + return Promise.resolve(undefined); + } } module.exports = UserStoreMock; diff --git a/src/test/fixtures/no-logger.ts b/src/test/fixtures/no-logger.ts index 9192933c36..35c5df361e 100644 --- a/src/test/fixtures/no-logger.ts +++ b/src/test/fixtures/no-logger.ts @@ -15,7 +15,7 @@ function noLoggerProvider(): Logger { }; } -noLoggerProvider.setMuteError = mute => { +noLoggerProvider.setMuteError = (mute) => { muteError = mute; }; diff --git a/src/test/fixtures/store.js b/src/test/fixtures/store.js deleted file mode 100644 index 29cd7e53fe..0000000000 --- a/src/test/fixtures/store.js +++ /dev/null @@ -1,52 +0,0 @@ -'use strict'; - -const FakeFeatureStrategiesStore = require('./fake-feature-strategies-store'); -const ClientMetricsStore = require('./fake-metrics-store'); -const clientInstanceStore = require('./fake-client-instance-store'); -const clientApplicationsStore = require('./fake-client-applications-store'); -const featureToggleStore = require('./fake-feature-toggle-store'); -const tagStore = require('./fake-tag-store'); -const tagTypeStore = require('./fake-tag-type-store'); -const EventStore = require('./fake-event-store'); -const strategyStore = require('./fake-strategies-store'); -const contextFieldStore = require('./fake-context-store'); -const settingStore = require('./fake-setting-store'); -const addonStore = require('./fake-addon-store'); -const projectStore = require('./fake-project-store'); -const UserStore = require('./fake-user-store'); -const AccessStore = require('./fake-access-store'); -const userFeedbackStore = require('./fake-user-feedback-store'); -const FakeFeatureTagStore = require('./fake-feature-tag-store'); -const FakeEnvironmentStore = require('./fake-environment-store'); - -module.exports = { - createStores: (databaseIsUp = true) => { - const db = { - select: () => ({ - from: () => Promise.resolve(), - }), - }; - - return { - db, - clientApplicationsStore: clientApplicationsStore(databaseIsUp), - clientMetricsStore: new ClientMetricsStore(databaseIsUp), - clientInstanceStore: clientInstanceStore(databaseIsUp), - featureToggleStore: featureToggleStore(databaseIsUp), - tagStore: tagStore(databaseIsUp), - tagTypeStore: tagTypeStore(databaseIsUp), - eventStore: new EventStore(databaseIsUp), - strategyStore: strategyStore(databaseIsUp), - contextFieldStore: contextFieldStore(databaseIsUp), - settingStore: settingStore(databaseIsUp), - addonStore: addonStore(databaseIsUp), - projectStore: projectStore(databaseIsUp), - userStore: new UserStore(), - accessStore: new AccessStore(), - userFeedbackStore: userFeedbackStore(databaseIsUp), - featureStrategiesStore: new FakeFeatureStrategiesStore(), - featureTagStore: new FakeFeatureTagStore(), - environmentStore: new FakeEnvironmentStore(), - }; - }, -}; diff --git a/src/test/fixtures/store.ts b/src/test/fixtures/store.ts new file mode 100644 index 0000000000..ccfb75d149 --- /dev/null +++ b/src/test/fixtures/store.ts @@ -0,0 +1,61 @@ +import FakeFeatureStrategiesStore from './fake-feature-strategies-store'; +import FakeClientMetricsStore from './fake-client-metrics-store'; +import FakeClientInstanceStore from './fake-client-instance-store'; +import FakeClientApplicationsStore from './fake-client-applications-store'; +import FakeFeatureToggleStore from './fake-feature-toggle-store'; +import FakeTagStore from './fake-tag-store'; +import FakeTagTypeStore from './fake-tag-type-store'; +import FakeEventStore from './fake-event-store'; +import FakeContextFieldStore from './fake-context-field-store'; +import FakeSettingStore from './fake-setting-store'; +import FakeAddonStore from './fake-addon-store'; +import FakeProjectStore from './fake-project-store'; +import FakeUserStore from './fake-user-store'; +import FakeAccessStore from './fake-access-store'; +import FakeUserFeedbackStore from './fake-user-feedback-store'; +import FakeFeatureTagStore from './fake-feature-tag-store'; +import FakeEnvironmentStore from './fake-environment-store'; +import FakeStrategiesStore from './fake-strategies-store'; +import { IUnleashStores } from '../../lib/types'; +import FakeSessionStore from './fake-session-store'; +import FakeFeatureEnvironmentStore from './fake-feature-environment-store'; +import FakeApiTokenStore from './fake-api-token-store'; +import FakeFeatureTypeStore from './fake-feature-type-store'; +import FakeResetTokenStore from './fake-reset-token-store'; + +const createStores: () => IUnleashStores = () => { + const db = { + select: () => ({ + from: () => Promise.resolve(), + }), + }; + + return { + db, + clientApplicationsStore: new FakeClientApplicationsStore(), + clientMetricsStore: new FakeClientMetricsStore(), + clientInstanceStore: new FakeClientInstanceStore(), + featureToggleStore: new FakeFeatureToggleStore(), + tagStore: new FakeTagStore(), + tagTypeStore: new FakeTagTypeStore(), + eventStore: new FakeEventStore(), + strategyStore: new FakeStrategiesStore(), + contextFieldStore: new FakeContextFieldStore(), + settingStore: new FakeSettingStore(), + addonStore: new FakeAddonStore(), + projectStore: new FakeProjectStore(), + userStore: new FakeUserStore(), + accessStore: new FakeAccessStore(), + userFeedbackStore: new FakeUserFeedbackStore(), + featureStrategiesStore: new FakeFeatureStrategiesStore(), + featureTagStore: new FakeFeatureTagStore(), + environmentStore: new FakeEnvironmentStore(), + featureEnvironmentStore: new FakeFeatureEnvironmentStore(), + apiTokenStore: new FakeApiTokenStore(), + featureTypeStore: new FakeFeatureTypeStore(), + resetTokenStore: new FakeResetTokenStore(), + sessionStore: new FakeSessionStore(), + }; +}; + +export default createStores; diff --git a/tsconfig.json b/tsconfig.json index bcf1419b26..20c2ce8560 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -41,7 +41,7 @@ // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ /* Module Resolution Options */ // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ - // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + "baseUrl": "./src" /* Base directory to resolve non-absolute module names. */, // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ // "typeRoots": [], /* List of folders to include type definitions from. */ @@ -75,6 +75,7 @@ "dist", "snapshots", "coverage", - "websitev2" + "websitev2", + "setupJest.js" ] } diff --git a/websitev2/docs/api/oas/swagger-ui-bundle.js b/websitev2/docs/api/oas/swagger-ui-bundle.js index a2f842e5aa..333ca0bd52 100644 --- a/websitev2/docs/api/oas/swagger-ui-bundle.js +++ b/websitev2/docs/api/oas/swagger-ui-bundle.js @@ -1,3 +1,3 @@ /*! For license information please see swagger-ui-bundle.js.LICENSE.txt */ -!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(function(){try{return require("esprima")}catch(e){}}()):"function"==typeof define&&define.amd?define(["esprima"],t):"object"==typeof exports?exports.SwaggerUIBundle=t(function(){try{return require("esprima")}catch(e){}}()):e.SwaggerUIBundle=t(e.esprima)}(this,(function(e){return function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="/dist",n(n.s=540)}([function(e,t,n){"use strict";e.exports=n(127)},function(e,t,n){e.exports=function(){"use strict";var e=Array.prototype.slice;function t(e,t){t&&(e.prototype=Object.create(t.prototype)),e.prototype.constructor=e}function n(e){return i(e)?e:$(e)}function r(e){return s(e)?e:K(e)}function o(e){return u(e)?e:Y(e)}function a(e){return i(e)&&!c(e)?e:G(e)}function i(e){return!(!e||!e[p])}function s(e){return!(!e||!e[f])}function u(e){return!(!e||!e[h])}function c(e){return s(e)||u(e)}function l(e){return!(!e||!e[d])}t(r,n),t(o,n),t(a,n),n.isIterable=i,n.isKeyed=s,n.isIndexed=u,n.isAssociative=c,n.isOrdered=l,n.Keyed=r,n.Indexed=o,n.Set=a;var p="@@__IMMUTABLE_ITERABLE__@@",f="@@__IMMUTABLE_KEYED__@@",h="@@__IMMUTABLE_INDEXED__@@",d="@@__IMMUTABLE_ORDERED__@@",v="delete",m=5,g=1<>>0;if(""+n!==t||4294967295===n)return NaN;t=n}return t<0?A(e)+t:t}function O(){return!0}function j(e,t,n){return(0===e||void 0!==n&&e<=-n)&&(void 0===t||void 0!==n&&t>=n)}function T(e,t){return N(e,t,0)}function I(e,t){return N(e,t,t)}function N(e,t,n){return void 0===e?n:e<0?Math.max(0,t+e):void 0===t?e:Math.min(t,e)}var M=0,P=1,R=2,D="function"==typeof Symbol&&Symbol.iterator,L="@@iterator",B=D||L;function F(e){this.next=e}function U(e,t,n,r){var o=0===e?t:1===e?n:[t,n];return r?r.value=o:r={value:o,done:!1},r}function q(){return{value:void 0,done:!0}}function z(e){return!!H(e)}function V(e){return e&&"function"==typeof e.next}function W(e){var t=H(e);return t&&t.call(e)}function H(e){var t=e&&(D&&e[D]||e[L]);if("function"==typeof t)return t}function J(e){return e&&"number"==typeof e.length}function $(e){return null==e?ie():i(e)?e.toSeq():ce(e)}function K(e){return null==e?ie().toKeyedSeq():i(e)?s(e)?e.toSeq():e.fromEntrySeq():se(e)}function Y(e){return null==e?ie():i(e)?s(e)?e.entrySeq():e.toIndexedSeq():ue(e)}function G(e){return(null==e?ie():i(e)?s(e)?e.entrySeq():e:ue(e)).toSetSeq()}F.prototype.toString=function(){return"[Iterator]"},F.KEYS=M,F.VALUES=P,F.ENTRIES=R,F.prototype.inspect=F.prototype.toSource=function(){return this.toString()},F.prototype[B]=function(){return this},t($,n),$.of=function(){return $(arguments)},$.prototype.toSeq=function(){return this},$.prototype.toString=function(){return this.__toString("Seq {","}")},$.prototype.cacheResult=function(){return!this._cache&&this.__iterateUncached&&(this._cache=this.entrySeq().toArray(),this.size=this._cache.length),this},$.prototype.__iterate=function(e,t){return pe(this,e,t,!0)},$.prototype.__iterator=function(e,t){return fe(this,e,t,!0)},t(K,$),K.prototype.toKeyedSeq=function(){return this},t(Y,$),Y.of=function(){return Y(arguments)},Y.prototype.toIndexedSeq=function(){return this},Y.prototype.toString=function(){return this.__toString("Seq [","]")},Y.prototype.__iterate=function(e,t){return pe(this,e,t,!1)},Y.prototype.__iterator=function(e,t){return fe(this,e,t,!1)},t(G,$),G.of=function(){return G(arguments)},G.prototype.toSetSeq=function(){return this},$.isSeq=ae,$.Keyed=K,$.Set=G,$.Indexed=Y;var Z,X,Q,ee="@@__IMMUTABLE_SEQ__@@";function te(e){this._array=e,this.size=e.length}function ne(e){var t=Object.keys(e);this._object=e,this._keys=t,this.size=t.length}function re(e){this._iterable=e,this.size=e.length||e.size}function oe(e){this._iterator=e,this._iteratorCache=[]}function ae(e){return!(!e||!e[ee])}function ie(){return Z||(Z=new te([]))}function se(e){var t=Array.isArray(e)?new te(e).fromEntrySeq():V(e)?new oe(e).fromEntrySeq():z(e)?new re(e).fromEntrySeq():"object"==typeof e?new ne(e):void 0;if(!t)throw new TypeError("Expected Array or iterable object of [k, v] entries, or keyed object: "+e);return t}function ue(e){var t=le(e);if(!t)throw new TypeError("Expected Array or iterable object of values: "+e);return t}function ce(e){var t=le(e)||"object"==typeof e&&new ne(e);if(!t)throw new TypeError("Expected Array or iterable object of values, or keyed object: "+e);return t}function le(e){return J(e)?new te(e):V(e)?new oe(e):z(e)?new re(e):void 0}function pe(e,t,n,r){var o=e._cache;if(o){for(var a=o.length-1,i=0;i<=a;i++){var s=o[n?a-i:i];if(!1===t(s[1],r?s[0]:i,e))return i+1}return i}return e.__iterateUncached(t,n)}function fe(e,t,n,r){var o=e._cache;if(o){var a=o.length-1,i=0;return new F((function(){var e=o[n?a-i:i];return i++>a?q():U(t,r?e[0]:i-1,e[1])}))}return e.__iteratorUncached(t,n)}function he(e,t){return t?de(t,e,"",{"":e}):ve(e)}function de(e,t,n,r){return Array.isArray(t)?e.call(r,n,Y(t).map((function(n,r){return de(e,n,r,t)}))):me(t)?e.call(r,n,K(t).map((function(n,r){return de(e,n,r,t)}))):t}function ve(e){return Array.isArray(e)?Y(e).map(ve).toList():me(e)?K(e).map(ve).toMap():e}function me(e){return e&&(e.constructor===Object||void 0===e.constructor)}function ge(e,t){if(e===t||e!=e&&t!=t)return!0;if(!e||!t)return!1;if("function"==typeof e.valueOf&&"function"==typeof t.valueOf){if((e=e.valueOf())===(t=t.valueOf())||e!=e&&t!=t)return!0;if(!e||!t)return!1}return!("function"!=typeof e.equals||"function"!=typeof t.equals||!e.equals(t))}function ye(e,t){if(e===t)return!0;if(!i(t)||void 0!==e.size&&void 0!==t.size&&e.size!==t.size||void 0!==e.__hash&&void 0!==t.__hash&&e.__hash!==t.__hash||s(e)!==s(t)||u(e)!==u(t)||l(e)!==l(t))return!1;if(0===e.size&&0===t.size)return!0;var n=!c(e);if(l(e)){var r=e.entries();return t.every((function(e,t){var o=r.next().value;return o&&ge(o[1],e)&&(n||ge(o[0],t))}))&&r.next().done}var o=!1;if(void 0===e.size)if(void 0===t.size)"function"==typeof e.cacheResult&&e.cacheResult();else{o=!0;var a=e;e=t,t=a}var p=!0,f=t.__iterate((function(t,r){if(n?!e.has(t):o?!ge(t,e.get(r,b)):!ge(e.get(r,b),t))return p=!1,!1}));return p&&e.size===f}function be(e,t){if(!(this instanceof be))return new be(e,t);if(this._value=e,this.size=void 0===t?1/0:Math.max(0,t),0===this.size){if(X)return X;X=this}}function _e(e,t){if(!e)throw new Error(t)}function we(e,t,n){if(!(this instanceof we))return new we(e,t,n);if(_e(0!==n,"Cannot step a Range by 0"),e=e||0,void 0===t&&(t=1/0),n=void 0===n?1:Math.abs(n),tr?q():U(e,o,n[t?r-o++:o++])}))},t(ne,K),ne.prototype.get=function(e,t){return void 0===t||this.has(e)?this._object[e]:t},ne.prototype.has=function(e){return this._object.hasOwnProperty(e)},ne.prototype.__iterate=function(e,t){for(var n=this._object,r=this._keys,o=r.length-1,a=0;a<=o;a++){var i=r[t?o-a:a];if(!1===e(n[i],i,this))return a+1}return a},ne.prototype.__iterator=function(e,t){var n=this._object,r=this._keys,o=r.length-1,a=0;return new F((function(){var i=r[t?o-a:a];return a++>o?q():U(e,i,n[i])}))},ne.prototype[d]=!0,t(re,Y),re.prototype.__iterateUncached=function(e,t){if(t)return this.cacheResult().__iterate(e,t);var n=W(this._iterable),r=0;if(V(n))for(var o;!(o=n.next()).done&&!1!==e(o.value,r++,this););return r},re.prototype.__iteratorUncached=function(e,t){if(t)return this.cacheResult().__iterator(e,t);var n=W(this._iterable);if(!V(n))return new F(q);var r=0;return new F((function(){var t=n.next();return t.done?t:U(e,r++,t.value)}))},t(oe,Y),oe.prototype.__iterateUncached=function(e,t){if(t)return this.cacheResult().__iterate(e,t);for(var n,r=this._iterator,o=this._iteratorCache,a=0;a=r.length){var t=n.next();if(t.done)return t;r[o]=t.value}return U(e,o,r[o++])}))},t(be,Y),be.prototype.toString=function(){return 0===this.size?"Repeat []":"Repeat [ "+this._value+" "+this.size+" times ]"},be.prototype.get=function(e,t){return this.has(e)?this._value:t},be.prototype.includes=function(e){return ge(this._value,e)},be.prototype.slice=function(e,t){var n=this.size;return j(e,t,n)?this:new be(this._value,I(t,n)-T(e,n))},be.prototype.reverse=function(){return this},be.prototype.indexOf=function(e){return ge(this._value,e)?0:-1},be.prototype.lastIndexOf=function(e){return ge(this._value,e)?this.size:-1},be.prototype.__iterate=function(e,t){for(var n=0;n=0&&t=0&&nn?q():U(e,a++,i)}))},we.prototype.equals=function(e){return e instanceof we?this._start===e._start&&this._end===e._end&&this._step===e._step:ye(this,e)},t(xe,n),t(Ee,xe),t(Ce,xe),t(Se,xe),xe.Keyed=Ee,xe.Indexed=Ce,xe.Set=Se;var Ae="function"==typeof Math.imul&&-2===Math.imul(4294967295,2)?Math.imul:function(e,t){var n=65535&(e|=0),r=65535&(t|=0);return n*r+((e>>>16)*r+n*(t>>>16)<<16>>>0)|0};function ke(e){return e>>>1&1073741824|3221225471&e}function Oe(e){if(!1===e||null==e)return 0;if("function"==typeof e.valueOf&&(!1===(e=e.valueOf())||null==e))return 0;if(!0===e)return 1;var t=typeof e;if("number"===t){if(e!=e||e===1/0)return 0;var n=0|e;for(n!==e&&(n^=4294967295*e);e>4294967295;)n^=e/=4294967295;return ke(n)}if("string"===t)return e.length>Fe?je(e):Te(e);if("function"==typeof e.hashCode)return e.hashCode();if("object"===t)return Ie(e);if("function"==typeof e.toString)return Te(e.toString());throw new Error("Value type "+t+" cannot be hashed.")}function je(e){var t=ze[e];return void 0===t&&(t=Te(e),qe===Ue&&(qe=0,ze={}),qe++,ze[e]=t),t}function Te(e){for(var t=0,n=0;n0)switch(e.nodeType){case 1:return e.uniqueID;case 9:return e.documentElement&&e.documentElement.uniqueID}}var Re,De="function"==typeof WeakMap;De&&(Re=new WeakMap);var Le=0,Be="__immutablehash__";"function"==typeof Symbol&&(Be=Symbol(Be));var Fe=16,Ue=255,qe=0,ze={};function Ve(e){_e(e!==1/0,"Cannot perform this action with an infinite size.")}function We(e){return null==e?ot():He(e)&&!l(e)?e:ot().withMutations((function(t){var n=r(e);Ve(n.size),n.forEach((function(e,n){return t.set(n,e)}))}))}function He(e){return!(!e||!e[$e])}t(We,Ee),We.of=function(){var t=e.call(arguments,0);return ot().withMutations((function(e){for(var n=0;n=t.length)throw new Error("Missing value for key: "+t[n]);e.set(t[n],t[n+1])}}))},We.prototype.toString=function(){return this.__toString("Map {","}")},We.prototype.get=function(e,t){return this._root?this._root.get(0,void 0,e,t):t},We.prototype.set=function(e,t){return at(this,e,t)},We.prototype.setIn=function(e,t){return this.updateIn(e,b,(function(){return t}))},We.prototype.remove=function(e){return at(this,e,b)},We.prototype.deleteIn=function(e){return this.updateIn(e,(function(){return b}))},We.prototype.update=function(e,t,n){return 1===arguments.length?e(this):this.updateIn([e],t,n)},We.prototype.updateIn=function(e,t,n){n||(n=t,t=void 0);var r=mt(this,xn(e),t,n);return r===b?void 0:r},We.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._root=null,this.__hash=void 0,this.__altered=!0,this):ot()},We.prototype.merge=function(){return ft(this,void 0,arguments)},We.prototype.mergeWith=function(t){return ft(this,t,e.call(arguments,1))},We.prototype.mergeIn=function(t){var n=e.call(arguments,1);return this.updateIn(t,ot(),(function(e){return"function"==typeof e.merge?e.merge.apply(e,n):n[n.length-1]}))},We.prototype.mergeDeep=function(){return ft(this,ht,arguments)},We.prototype.mergeDeepWith=function(t){var n=e.call(arguments,1);return ft(this,dt(t),n)},We.prototype.mergeDeepIn=function(t){var n=e.call(arguments,1);return this.updateIn(t,ot(),(function(e){return"function"==typeof e.mergeDeep?e.mergeDeep.apply(e,n):n[n.length-1]}))},We.prototype.sort=function(e){return zt(pn(this,e))},We.prototype.sortBy=function(e,t){return zt(pn(this,t,e))},We.prototype.withMutations=function(e){var t=this.asMutable();return e(t),t.wasAltered()?t.__ensureOwner(this.__ownerID):this},We.prototype.asMutable=function(){return this.__ownerID?this:this.__ensureOwner(new C)},We.prototype.asImmutable=function(){return this.__ensureOwner()},We.prototype.wasAltered=function(){return this.__altered},We.prototype.__iterator=function(e,t){return new et(this,e,t)},We.prototype.__iterate=function(e,t){var n=this,r=0;return this._root&&this._root.iterate((function(t){return r++,e(t[1],t[0],n)}),t),r},We.prototype.__ensureOwner=function(e){return e===this.__ownerID?this:e?rt(this.size,this._root,e,this.__hash):(this.__ownerID=e,this.__altered=!1,this)},We.isMap=He;var Je,$e="@@__IMMUTABLE_MAP__@@",Ke=We.prototype;function Ye(e,t){this.ownerID=e,this.entries=t}function Ge(e,t,n){this.ownerID=e,this.bitmap=t,this.nodes=n}function Ze(e,t,n){this.ownerID=e,this.count=t,this.nodes=n}function Xe(e,t,n){this.ownerID=e,this.keyHash=t,this.entries=n}function Qe(e,t,n){this.ownerID=e,this.keyHash=t,this.entry=n}function et(e,t,n){this._type=t,this._reverse=n,this._stack=e._root&&nt(e._root)}function tt(e,t){return U(e,t[0],t[1])}function nt(e,t){return{node:e,index:0,__prev:t}}function rt(e,t,n,r){var o=Object.create(Ke);return o.size=e,o._root=t,o.__ownerID=n,o.__hash=r,o.__altered=!1,o}function ot(){return Je||(Je=rt(0))}function at(e,t,n){var r,o;if(e._root){var a=x(_),i=x(w);if(r=it(e._root,e.__ownerID,0,void 0,t,n,a,i),!i.value)return e;o=e.size+(a.value?n===b?-1:1:0)}else{if(n===b)return e;o=1,r=new Ye(e.__ownerID,[[t,n]])}return e.__ownerID?(e.size=o,e._root=r,e.__hash=void 0,e.__altered=!0,e):r?rt(o,r):ot()}function it(e,t,n,r,o,a,i,s){return e?e.update(t,n,r,o,a,i,s):a===b?e:(E(s),E(i),new Qe(t,r,[o,a]))}function st(e){return e.constructor===Qe||e.constructor===Xe}function ut(e,t,n,r,o){if(e.keyHash===r)return new Xe(t,r,[e.entry,o]);var a,i=(0===n?e.keyHash:e.keyHash>>>n)&y,s=(0===n?r:r>>>n)&y;return new Ge(t,1<>>=1)i[s]=1&n?t[a++]:void 0;return i[r]=o,new Ze(e,a+1,i)}function ft(e,t,n){for(var o=[],a=0;a>1&1431655765))+(e>>2&858993459))+(e>>4)&252645135,e+=e>>8,127&(e+=e>>16)}function yt(e,t,n,r){var o=r?e:S(e);return o[t]=n,o}function bt(e,t,n,r){var o=e.length+1;if(r&&t+1===o)return e[t]=n,e;for(var a=new Array(o),i=0,s=0;s=wt)return ct(e,u,r,o);var f=e&&e===this.ownerID,h=f?u:S(u);return p?s?c===l-1?h.pop():h[c]=h.pop():h[c]=[r,o]:h.push([r,o]),f?(this.entries=h,this):new Ye(e,h)}},Ge.prototype.get=function(e,t,n,r){void 0===t&&(t=Oe(n));var o=1<<((0===e?t:t>>>e)&y),a=this.bitmap;return 0==(a&o)?r:this.nodes[gt(a&o-1)].get(e+m,t,n,r)},Ge.prototype.update=function(e,t,n,r,o,a,i){void 0===n&&(n=Oe(r));var s=(0===t?n:n>>>t)&y,u=1<=xt)return pt(e,f,c,s,d);if(l&&!d&&2===f.length&&st(f[1^p]))return f[1^p];if(l&&d&&1===f.length&&st(d))return d;var v=e&&e===this.ownerID,g=l?d?c:c^u:c|u,_=l?d?yt(f,p,d,v):_t(f,p,v):bt(f,p,d,v);return v?(this.bitmap=g,this.nodes=_,this):new Ge(e,g,_)},Ze.prototype.get=function(e,t,n,r){void 0===t&&(t=Oe(n));var o=(0===e?t:t>>>e)&y,a=this.nodes[o];return a?a.get(e+m,t,n,r):r},Ze.prototype.update=function(e,t,n,r,o,a,i){void 0===n&&(n=Oe(r));var s=(0===t?n:n>>>t)&y,u=o===b,c=this.nodes,l=c[s];if(u&&!l)return this;var p=it(l,e,t+m,n,r,o,a,i);if(p===l)return this;var f=this.count;if(l){if(!p&&--f0&&r=0&&e>>t&y;if(r>=this.array.length)return new Ot([],e);var o,a=0===r;if(t>0){var i=this.array[r];if((o=i&&i.removeBefore(e,t-m,n))===i&&a)return this}if(a&&!o)return this;var s=Lt(this,e);if(!a)for(var u=0;u>>t&y;if(o>=this.array.length)return this;if(t>0){var a=this.array[o];if((r=a&&a.removeAfter(e,t-m,n))===a&&o===this.array.length-1)return this}var i=Lt(this,e);return i.array.splice(o+1),r&&(i.array[o]=r),i};var jt,Tt,It={};function Nt(e,t){var n=e._origin,r=e._capacity,o=qt(r),a=e._tail;return i(e._root,e._level,0);function i(e,t,n){return 0===t?s(e,n):u(e,t,n)}function s(e,i){var s=i===o?a&&a.array:e&&e.array,u=i>n?0:n-i,c=r-i;return c>g&&(c=g),function(){if(u===c)return It;var e=t?--c:u++;return s&&s[e]}}function u(e,o,a){var s,u=e&&e.array,c=a>n?0:n-a>>o,l=1+(r-a>>o);return l>g&&(l=g),function(){for(;;){if(s){var e=s();if(e!==It)return e;s=null}if(c===l)return It;var n=t?--l:c++;s=i(u&&u[n],o-m,a+(n<=e.size||t<0)return e.withMutations((function(e){t<0?Ft(e,t).set(0,n):Ft(e,0,t+1).set(t,n)}));t+=e._origin;var r=e._tail,o=e._root,a=x(w);return t>=qt(e._capacity)?r=Dt(r,e.__ownerID,0,t,n,a):o=Dt(o,e.__ownerID,e._level,t,n,a),a.value?e.__ownerID?(e._root=o,e._tail=r,e.__hash=void 0,e.__altered=!0,e):Mt(e._origin,e._capacity,e._level,o,r):e}function Dt(e,t,n,r,o,a){var i,s=r>>>n&y,u=e&&s0){var c=e&&e.array[s],l=Dt(c,t,n-m,r,o,a);return l===c?e:((i=Lt(e,t)).array[s]=l,i)}return u&&e.array[s]===o?e:(E(a),i=Lt(e,t),void 0===o&&s===i.array.length-1?i.array.pop():i.array[s]=o,i)}function Lt(e,t){return t&&e&&t===e.ownerID?e:new Ot(e?e.array.slice():[],t)}function Bt(e,t){if(t>=qt(e._capacity))return e._tail;if(t<1<0;)n=n.array[t>>>r&y],r-=m;return n}}function Ft(e,t,n){void 0!==t&&(t|=0),void 0!==n&&(n|=0);var r=e.__ownerID||new C,o=e._origin,a=e._capacity,i=o+t,s=void 0===n?a:n<0?a+n:o+n;if(i===o&&s===a)return e;if(i>=s)return e.clear();for(var u=e._level,c=e._root,l=0;i+l<0;)c=new Ot(c&&c.array.length?[void 0,c]:[],r),l+=1<<(u+=m);l&&(i+=l,o+=l,s+=l,a+=l);for(var p=qt(a),f=qt(s);f>=1<p?new Ot([],r):h;if(h&&f>p&&im;g-=m){var b=p>>>g&y;v=v.array[b]=Lt(v.array[b],r)}v.array[p>>>m&y]=h}if(s=f)i-=f,s-=f,u=m,c=null,d=d&&d.removeBefore(r,0,i);else if(i>o||f>>u&y;if(_!==f>>>u&y)break;_&&(l+=(1<o&&(c=c.removeBefore(r,u,i-l)),c&&fa&&(a=c.size),i(u)||(c=c.map((function(e){return he(e)}))),r.push(c)}return a>e.size&&(e=e.setSize(a)),vt(e,t,r)}function qt(e){return e>>m<=g&&i.size>=2*a.size?(r=(o=i.filter((function(e,t){return void 0!==e&&s!==t}))).toKeyedSeq().map((function(e){return e[0]})).flip().toMap(),e.__ownerID&&(r.__ownerID=o.__ownerID=e.__ownerID)):(r=a.remove(t),o=s===i.size-1?i.pop():i.set(s,void 0))}else if(u){if(n===i.get(s)[1])return e;r=a,o=i.set(s,[t,n])}else r=a.set(t,i.size),o=i.set(i.size,[t,n]);return e.__ownerID?(e.size=r.size,e._map=r,e._list=o,e.__hash=void 0,e):Wt(r,o)}function $t(e,t){this._iter=e,this._useKeys=t,this.size=e.size}function Kt(e){this._iter=e,this.size=e.size}function Yt(e){this._iter=e,this.size=e.size}function Gt(e){this._iter=e,this.size=e.size}function Zt(e){var t=bn(e);return t._iter=e,t.size=e.size,t.flip=function(){return e},t.reverse=function(){var t=e.reverse.apply(this);return t.flip=function(){return e.reverse()},t},t.has=function(t){return e.includes(t)},t.includes=function(t){return e.has(t)},t.cacheResult=_n,t.__iterateUncached=function(t,n){var r=this;return e.__iterate((function(e,n){return!1!==t(n,e,r)}),n)},t.__iteratorUncached=function(t,n){if(t===R){var r=e.__iterator(t,n);return new F((function(){var e=r.next();if(!e.done){var t=e.value[0];e.value[0]=e.value[1],e.value[1]=t}return e}))}return e.__iterator(t===P?M:P,n)},t}function Xt(e,t,n){var r=bn(e);return r.size=e.size,r.has=function(t){return e.has(t)},r.get=function(r,o){var a=e.get(r,b);return a===b?o:t.call(n,a,r,e)},r.__iterateUncached=function(r,o){var a=this;return e.__iterate((function(e,o,i){return!1!==r(t.call(n,e,o,i),o,a)}),o)},r.__iteratorUncached=function(r,o){var a=e.__iterator(R,o);return new F((function(){var o=a.next();if(o.done)return o;var i=o.value,s=i[0];return U(r,s,t.call(n,i[1],s,e),o)}))},r}function Qt(e,t){var n=bn(e);return n._iter=e,n.size=e.size,n.reverse=function(){return e},e.flip&&(n.flip=function(){var t=Zt(e);return t.reverse=function(){return e.flip()},t}),n.get=function(n,r){return e.get(t?n:-1-n,r)},n.has=function(n){return e.has(t?n:-1-n)},n.includes=function(t){return e.includes(t)},n.cacheResult=_n,n.__iterate=function(t,n){var r=this;return e.__iterate((function(e,n){return t(e,n,r)}),!n)},n.__iterator=function(t,n){return e.__iterator(t,!n)},n}function en(e,t,n,r){var o=bn(e);return r&&(o.has=function(r){var o=e.get(r,b);return o!==b&&!!t.call(n,o,r,e)},o.get=function(r,o){var a=e.get(r,b);return a!==b&&t.call(n,a,r,e)?a:o}),o.__iterateUncached=function(o,a){var i=this,s=0;return e.__iterate((function(e,a,u){if(t.call(n,e,a,u))return s++,o(e,r?a:s-1,i)}),a),s},o.__iteratorUncached=function(o,a){var i=e.__iterator(R,a),s=0;return new F((function(){for(;;){var a=i.next();if(a.done)return a;var u=a.value,c=u[0],l=u[1];if(t.call(n,l,c,e))return U(o,r?c:s++,l,a)}}))},o}function tn(e,t,n){var r=We().asMutable();return e.__iterate((function(o,a){r.update(t.call(n,o,a,e),0,(function(e){return e+1}))})),r.asImmutable()}function nn(e,t,n){var r=s(e),o=(l(e)?zt():We()).asMutable();e.__iterate((function(a,i){o.update(t.call(n,a,i,e),(function(e){return(e=e||[]).push(r?[i,a]:a),e}))}));var a=yn(e);return o.map((function(t){return vn(e,a(t))}))}function rn(e,t,n,r){var o=e.size;if(void 0!==t&&(t|=0),void 0!==n&&(n===1/0?n=o:n|=0),j(t,n,o))return e;var a=T(t,o),i=I(n,o);if(a!=a||i!=i)return rn(e.toSeq().cacheResult(),t,n,r);var s,u=i-a;u==u&&(s=u<0?0:u);var c=bn(e);return c.size=0===s?s:e.size&&s||void 0,!r&&ae(e)&&s>=0&&(c.get=function(t,n){return(t=k(this,t))>=0&&ts)return q();var e=o.next();return r||t===P?e:U(t,u-1,t===M?void 0:e.value[1],e)}))},c}function on(e,t,n){var r=bn(e);return r.__iterateUncached=function(r,o){var a=this;if(o)return this.cacheResult().__iterate(r,o);var i=0;return e.__iterate((function(e,o,s){return t.call(n,e,o,s)&&++i&&r(e,o,a)})),i},r.__iteratorUncached=function(r,o){var a=this;if(o)return this.cacheResult().__iterator(r,o);var i=e.__iterator(R,o),s=!0;return new F((function(){if(!s)return q();var e=i.next();if(e.done)return e;var o=e.value,u=o[0],c=o[1];return t.call(n,c,u,a)?r===R?e:U(r,u,c,e):(s=!1,q())}))},r}function an(e,t,n,r){var o=bn(e);return o.__iterateUncached=function(o,a){var i=this;if(a)return this.cacheResult().__iterate(o,a);var s=!0,u=0;return e.__iterate((function(e,a,c){if(!s||!(s=t.call(n,e,a,c)))return u++,o(e,r?a:u-1,i)})),u},o.__iteratorUncached=function(o,a){var i=this;if(a)return this.cacheResult().__iterator(o,a);var s=e.__iterator(R,a),u=!0,c=0;return new F((function(){var e,a,l;do{if((e=s.next()).done)return r||o===P?e:U(o,c++,o===M?void 0:e.value[1],e);var p=e.value;a=p[0],l=p[1],u&&(u=t.call(n,l,a,i))}while(u);return o===R?e:U(o,a,l,e)}))},o}function sn(e,t){var n=s(e),o=[e].concat(t).map((function(e){return i(e)?n&&(e=r(e)):e=n?se(e):ue(Array.isArray(e)?e:[e]),e})).filter((function(e){return 0!==e.size}));if(0===o.length)return e;if(1===o.length){var a=o[0];if(a===e||n&&s(a)||u(e)&&u(a))return a}var c=new te(o);return n?c=c.toKeyedSeq():u(e)||(c=c.toSetSeq()),(c=c.flatten(!0)).size=o.reduce((function(e,t){if(void 0!==e){var n=t.size;if(void 0!==n)return e+n}}),0),c}function un(e,t,n){var r=bn(e);return r.__iterateUncached=function(r,o){var a=0,s=!1;function u(e,c){var l=this;e.__iterate((function(e,o){return(!t||c0}function dn(e,t,r){var o=bn(e);return o.size=new te(r).map((function(e){return e.size})).min(),o.__iterate=function(e,t){for(var n,r=this.__iterator(P,t),o=0;!(n=r.next()).done&&!1!==e(n.value,o++,this););return o},o.__iteratorUncached=function(e,o){var a=r.map((function(e){return e=n(e),W(o?e.reverse():e)})),i=0,s=!1;return new F((function(){var n;return s||(n=a.map((function(e){return e.next()})),s=n.some((function(e){return e.done}))),s?q():U(e,i++,t.apply(null,n.map((function(e){return e.value}))))}))},o}function vn(e,t){return ae(e)?t:e.constructor(t)}function mn(e){if(e!==Object(e))throw new TypeError("Expected [K, V] tuple: "+e)}function gn(e){return Ve(e.size),A(e)}function yn(e){return s(e)?r:u(e)?o:a}function bn(e){return Object.create((s(e)?K:u(e)?Y:G).prototype)}function _n(){return this._iter.cacheResult?(this._iter.cacheResult(),this.size=this._iter.size,this):$.prototype.cacheResult.call(this)}function wn(e,t){return e>t?1:e=0;n--)t={value:arguments[n],next:t};return this.__ownerID?(this.size=e,this._head=t,this.__hash=void 0,this.__altered=!0,this):Kn(e,t)},Vn.prototype.pushAll=function(e){if(0===(e=o(e)).size)return this;Ve(e.size);var t=this.size,n=this._head;return e.reverse().forEach((function(e){t++,n={value:e,next:n}})),this.__ownerID?(this.size=t,this._head=n,this.__hash=void 0,this.__altered=!0,this):Kn(t,n)},Vn.prototype.pop=function(){return this.slice(1)},Vn.prototype.unshift=function(){return this.push.apply(this,arguments)},Vn.prototype.unshiftAll=function(e){return this.pushAll(e)},Vn.prototype.shift=function(){return this.pop.apply(this,arguments)},Vn.prototype.clear=function(){return 0===this.size?this:this.__ownerID?(this.size=0,this._head=void 0,this.__hash=void 0,this.__altered=!0,this):Yn()},Vn.prototype.slice=function(e,t){if(j(e,t,this.size))return this;var n=T(e,this.size);if(I(t,this.size)!==this.size)return Ce.prototype.slice.call(this,e,t);for(var r=this.size-n,o=this._head;n--;)o=o.next;return this.__ownerID?(this.size=r,this._head=o,this.__hash=void 0,this.__altered=!0,this):Kn(r,o)},Vn.prototype.__ensureOwner=function(e){return e===this.__ownerID?this:e?Kn(this.size,this._head,e,this.__hash):(this.__ownerID=e,this.__altered=!1,this)},Vn.prototype.__iterate=function(e,t){if(t)return this.reverse().__iterate(e);for(var n=0,r=this._head;r&&!1!==e(r.value,n++,this);)r=r.next;return n},Vn.prototype.__iterator=function(e,t){if(t)return this.reverse().__iterator(e);var n=0,r=this._head;return new F((function(){if(r){var t=r.value;return r=r.next,U(e,n++,t)}return q()}))},Vn.isStack=Wn;var Hn,Jn="@@__IMMUTABLE_STACK__@@",$n=Vn.prototype;function Kn(e,t,n,r){var o=Object.create($n);return o.size=e,o._head=t,o.__ownerID=n,o.__hash=r,o.__altered=!1,o}function Yn(){return Hn||(Hn=Kn(0))}function Gn(e,t){var n=function(n){e.prototype[n]=t[n]};return Object.keys(t).forEach(n),Object.getOwnPropertySymbols&&Object.getOwnPropertySymbols(t).forEach(n),e}$n[Jn]=!0,$n.withMutations=Ke.withMutations,$n.asMutable=Ke.asMutable,$n.asImmutable=Ke.asImmutable,$n.wasAltered=Ke.wasAltered,n.Iterator=F,Gn(n,{toArray:function(){Ve(this.size);var e=new Array(this.size||0);return this.valueSeq().__iterate((function(t,n){e[n]=t})),e},toIndexedSeq:function(){return new Kt(this)},toJS:function(){return this.toSeq().map((function(e){return e&&"function"==typeof e.toJS?e.toJS():e})).__toJS()},toJSON:function(){return this.toSeq().map((function(e){return e&&"function"==typeof e.toJSON?e.toJSON():e})).__toJS()},toKeyedSeq:function(){return new $t(this,!0)},toMap:function(){return We(this.toKeyedSeq())},toObject:function(){Ve(this.size);var e={};return this.__iterate((function(t,n){e[n]=t})),e},toOrderedMap:function(){return zt(this.toKeyedSeq())},toOrderedSet:function(){return Ln(s(this)?this.valueSeq():this)},toSet:function(){return jn(s(this)?this.valueSeq():this)},toSetSeq:function(){return new Yt(this)},toSeq:function(){return u(this)?this.toIndexedSeq():s(this)?this.toKeyedSeq():this.toSetSeq()},toStack:function(){return Vn(s(this)?this.valueSeq():this)},toList:function(){return Ct(s(this)?this.valueSeq():this)},toString:function(){return"[Iterable]"},__toString:function(e,t){return 0===this.size?e+t:e+" "+this.toSeq().map(this.__toStringMapper).join(", ")+" "+t},concat:function(){return vn(this,sn(this,e.call(arguments,0)))},includes:function(e){return this.some((function(t){return ge(t,e)}))},entries:function(){return this.__iterator(R)},every:function(e,t){Ve(this.size);var n=!0;return this.__iterate((function(r,o,a){if(!e.call(t,r,o,a))return n=!1,!1})),n},filter:function(e,t){return vn(this,en(this,e,t,!0))},find:function(e,t,n){var r=this.findEntry(e,t);return r?r[1]:n},forEach:function(e,t){return Ve(this.size),this.__iterate(t?e.bind(t):e)},join:function(e){Ve(this.size),e=void 0!==e?""+e:",";var t="",n=!0;return this.__iterate((function(r){n?n=!1:t+=e,t+=null!=r?r.toString():""})),t},keys:function(){return this.__iterator(M)},map:function(e,t){return vn(this,Xt(this,e,t))},reduce:function(e,t,n){var r,o;return Ve(this.size),arguments.length<2?o=!0:r=t,this.__iterate((function(t,a,i){o?(o=!1,r=t):r=e.call(n,r,t,a,i)})),r},reduceRight:function(e,t,n){var r=this.toKeyedSeq().reverse();return r.reduce.apply(r,arguments)},reverse:function(){return vn(this,Qt(this,!0))},slice:function(e,t){return vn(this,rn(this,e,t,!0))},some:function(e,t){return!this.every(tr(e),t)},sort:function(e){return vn(this,pn(this,e))},values:function(){return this.__iterator(P)},butLast:function(){return this.slice(0,-1)},isEmpty:function(){return void 0!==this.size?0===this.size:!this.some((function(){return!0}))},count:function(e,t){return A(e?this.toSeq().filter(e,t):this)},countBy:function(e,t){return tn(this,e,t)},equals:function(e){return ye(this,e)},entrySeq:function(){var e=this;if(e._cache)return new te(e._cache);var t=e.toSeq().map(er).toIndexedSeq();return t.fromEntrySeq=function(){return e.toSeq()},t},filterNot:function(e,t){return this.filter(tr(e),t)},findEntry:function(e,t,n){var r=n;return this.__iterate((function(n,o,a){if(e.call(t,n,o,a))return r=[o,n],!1})),r},findKey:function(e,t){var n=this.findEntry(e,t);return n&&n[0]},findLast:function(e,t,n){return this.toKeyedSeq().reverse().find(e,t,n)},findLastEntry:function(e,t,n){return this.toKeyedSeq().reverse().findEntry(e,t,n)},findLastKey:function(e,t){return this.toKeyedSeq().reverse().findKey(e,t)},first:function(){return this.find(O)},flatMap:function(e,t){return vn(this,cn(this,e,t))},flatten:function(e){return vn(this,un(this,e,!0))},fromEntrySeq:function(){return new Gt(this)},get:function(e,t){return this.find((function(t,n){return ge(n,e)}),void 0,t)},getIn:function(e,t){for(var n,r=this,o=xn(e);!(n=o.next()).done;){var a=n.value;if((r=r&&r.get?r.get(a,b):b)===b)return t}return r},groupBy:function(e,t){return nn(this,e,t)},has:function(e){return this.get(e,b)!==b},hasIn:function(e){return this.getIn(e,b)!==b},isSubset:function(e){return e="function"==typeof e.includes?e:n(e),this.every((function(t){return e.includes(t)}))},isSuperset:function(e){return(e="function"==typeof e.isSubset?e:n(e)).isSubset(this)},keyOf:function(e){return this.findKey((function(t){return ge(t,e)}))},keySeq:function(){return this.toSeq().map(Qn).toIndexedSeq()},last:function(){return this.toSeq().reverse().first()},lastKeyOf:function(e){return this.toKeyedSeq().reverse().keyOf(e)},max:function(e){return fn(this,e)},maxBy:function(e,t){return fn(this,t,e)},min:function(e){return fn(this,e?nr(e):ar)},minBy:function(e,t){return fn(this,t?nr(t):ar,e)},rest:function(){return this.slice(1)},skip:function(e){return this.slice(Math.max(0,e))},skipLast:function(e){return vn(this,this.toSeq().reverse().skip(e).reverse())},skipWhile:function(e,t){return vn(this,an(this,e,t,!0))},skipUntil:function(e,t){return this.skipWhile(tr(e),t)},sortBy:function(e,t){return vn(this,pn(this,t,e))},take:function(e){return this.slice(0,Math.max(0,e))},takeLast:function(e){return vn(this,this.toSeq().reverse().take(e).reverse())},takeWhile:function(e,t){return vn(this,on(this,e,t))},takeUntil:function(e,t){return this.takeWhile(tr(e),t)},valueSeq:function(){return this.toIndexedSeq()},hashCode:function(){return this.__hash||(this.__hash=ir(this))}});var Zn=n.prototype;Zn[p]=!0,Zn[B]=Zn.values,Zn.__toJS=Zn.toArray,Zn.__toStringMapper=rr,Zn.inspect=Zn.toSource=function(){return this.toString()},Zn.chain=Zn.flatMap,Zn.contains=Zn.includes,Gn(r,{flip:function(){return vn(this,Zt(this))},mapEntries:function(e,t){var n=this,r=0;return vn(this,this.toSeq().map((function(o,a){return e.call(t,[a,o],r++,n)})).fromEntrySeq())},mapKeys:function(e,t){var n=this;return vn(this,this.toSeq().flip().map((function(r,o){return e.call(t,r,o,n)})).flip())}});var Xn=r.prototype;function Qn(e,t){return t}function er(e,t){return[t,e]}function tr(e){return function(){return!e.apply(this,arguments)}}function nr(e){return function(){return-e.apply(this,arguments)}}function rr(e){return"string"==typeof e?JSON.stringify(e):String(e)}function or(){return S(arguments)}function ar(e,t){return et?-1:0}function ir(e){if(e.size===1/0)return 0;var t=l(e),n=s(e),r=t?1:0;return sr(e.__iterate(n?t?function(e,t){r=31*r+ur(Oe(e),Oe(t))|0}:function(e,t){r=r+ur(Oe(e),Oe(t))|0}:t?function(e){r=31*r+Oe(e)|0}:function(e){r=r+Oe(e)|0}),r)}function sr(e,t){return t=Ae(t,3432918353),t=Ae(t<<15|t>>>-15,461845907),t=Ae(t<<13|t>>>-13,5),t=Ae((t=(t+3864292196|0)^e)^t>>>16,2246822507),t=ke((t=Ae(t^t>>>13,3266489909))^t>>>16)}function ur(e,t){return e^t+2654435769+(e<<6)+(e>>2)|0}return Xn[f]=!0,Xn[B]=Zn.entries,Xn.__toJS=Zn.toObject,Xn.__toStringMapper=function(e,t){return JSON.stringify(t)+": "+rr(e)},Gn(o,{toKeyedSeq:function(){return new $t(this,!1)},filter:function(e,t){return vn(this,en(this,e,t,!1))},findIndex:function(e,t){var n=this.findEntry(e,t);return n?n[0]:-1},indexOf:function(e){var t=this.keyOf(e);return void 0===t?-1:t},lastIndexOf:function(e){var t=this.lastKeyOf(e);return void 0===t?-1:t},reverse:function(){return vn(this,Qt(this,!1))},slice:function(e,t){return vn(this,rn(this,e,t,!1))},splice:function(e,t){var n=arguments.length;if(t=Math.max(0|t,0),0===n||2===n&&!t)return this;e=T(e,e<0?this.count():this.size);var r=this.slice(0,e);return vn(this,1===n?r:r.concat(S(arguments,2),this.slice(e+t)))},findLastIndex:function(e,t){var n=this.findLastEntry(e,t);return n?n[0]:-1},first:function(){return this.get(0)},flatten:function(e){return vn(this,un(this,e,!1))},get:function(e,t){return(e=k(this,e))<0||this.size===1/0||void 0!==this.size&&e>this.size?t:this.find((function(t,n){return n===e}),void 0,t)},has:function(e){return(e=k(this,e))>=0&&(void 0!==this.size?this.size===1/0||e1)try{return decodeURIComponent(t[1])}catch(e){console.error(e)}return null}function Te(e){return t=e.replace(/\.[^./]*$/,""),$()(H()(t));var t}var Ie=function(e,t){if(e>t)return"Value must be less than ".concat(t)},Ne=function(e,t){if(et)return T()(n="Value must be no longer than ".concat(t," character")).call(n,1!==t?"s":"")},qe=function(e,t){var n;if(e.length2&&void 0!==arguments[2]?arguments[2]:{},r=n.isOAS3,o=void 0!==r&&r,a=n.bypassRequiredCheck,i=void 0!==a&&a,s=[],u=e.get("required"),c=Object(ue.a)(e,{isOAS3:o}),l=c.schema,p=c.parameterContentMediaType;if(!l)return s;var f=l.get("required"),h=l.get("maximum"),d=l.get("minimum"),v=l.get("type"),g=l.get("format"),y=l.get("maxLength"),b=l.get("minLength"),_=l.get("pattern");if(v&&(u||f||t)){var w="string"===v&&t,x="array"===v&&B()(t)&&t.length,E="array"===v&&z.a.List.isList(t)&&t.count(),C="array"===v&&"string"==typeof t&&t,S="file"===v&&t instanceof ae.a.File,A="boolean"===v&&(t||!1===t),k="number"===v&&(t||0===t),j="integer"===v&&(t||0===t),T="object"===v&&"object"===U()(t)&&null!==t,I="object"===v&&"string"==typeof t&&t,N=[w,x,E,C,S,A,k,j,T,I],M=m()(N).call(N,(function(e){return!!e}));if((u||f)&&!M&&!i)return s.push("Required field is not provided"),s;if("object"===v&&"string"==typeof t&&(null===p||"application/json"===p))try{JSON.parse(t)}catch(e){return s.push("Parameter string value must be valid JSON"),s}if(_){var P=ze(t,_);P&&s.push(P)}if(y||0===y){var R=Ue(t,y);R&&s.push(R)}if(b){var D=qe(t,b);D&&s.push(D)}if(h||0===h){var L=Ie(t,h);L&&s.push(L)}if(d||0===d){var F=Ne(t,d);F&&s.push(F)}if("string"===v){var q;if(!(q="date-time"===g?Be(t):"uuid"===g?Fe(t):Le(t)))return s;s.push(q)}else if("boolean"===v){var V=De(t);if(!V)return s;s.push(V)}else if("number"===v){var W=Me(t);if(!W)return s;s.push(W)}else if("integer"===v){var H=Pe(t);if(!H)return s;s.push(H)}else if("array"===v){var J;if(!E||!t.count())return s;J=l.getIn(["items","type"]),O()(t).call(t,(function(e,t){var n;"number"===J?n=Me(e):"integer"===J?n=Pe(e):"string"===J&&(n=Le(e)),n&&s.push({index:t,error:n})}))}else if("file"===v){var $=Re(t);if(!$)return s;s.push($)}}return s},We=function(e,t){if(!e.xml||!e.xml.name){if(e.xml=e.xml||{},!e.$$ref)return e.type||e.items||e.properties||e.additionalProperties?'\n\x3c!-- XML example cannot be generated; root element name is undefined --\x3e':null;var n=e.$$ref.match(/\S*\/(\S+)$/);e.xml.name=n[1]}return Object(oe.memoizedCreateXMLExample)(e,t)},He=[{when:/json/,shouldStringifyTypes:["string"]}],Je=["object"],$e=function(e,t,n){var r=Object(oe.memoizedSampleFromSchema)(e,t),o=U()(r),a=x()(He).call(He,(function(e,t){var r;return t.when.test(n)?T()(r=[]).call(r,d()(e),d()(t.shouldStringifyTypes)):e}),Je);return Q()(a,(function(e){return e===o}))?f()(r,null,2):r},Ke=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};return/xml/.test(t)?We(e,n):$e(e,n,t)},Ye=function(){var e={},t=ae.a.location.search;if(!t)return{};if(""!=t){var n=t.substr(1).split("&");for(var r in n)n.hasOwnProperty(r)&&(r=n[r].split("="),e[decodeURIComponent(r[0])]=r[1]&&decodeURIComponent(r[1])||"")}return e},Ge=function(t){return(t instanceof e?t:e.from(t.toString(),"utf-8")).toString("base64")},Ze={operationsSorter:{alpha:function(e,t){return e.get("path").localeCompare(t.get("path"))},method:function(e,t){return e.get("method").localeCompare(t.get("method"))}},tagsSorter:{alpha:function(e,t){return e.localeCompare(t)}}},Xe=function(e){var t=[];for(var n in e){var r=e[n];void 0!==r&&""!==r&&t.push([n,"=",encodeURIComponent(r).replace(/%20/g,"+")].join(""))}return t.join("&")},Qe=function(e,t,n){return!!Z()(n,(function(n){return te()(e[n],t[n])}))};function et(e){return"string"!=typeof e||""===e?"":Object(V.sanitizeUrl)(e)}function tt(e){return!(!e||l()(e).call(e,"localhost")>=0||l()(e).call(e,"127.0.0.1")>=0||"none"===e)}function nt(e){if(!z.a.OrderedMap.isOrderedMap(e))return null;if(!e.size)return null;var t=u()(e).call(e,(function(e,t){return i()(t).call(t,"2")&&C()(e.get("content")||{}).length>0})),n=e.get("default")||z.a.OrderedMap(),r=(n.get("content")||z.a.OrderedMap()).keySeq().toJS().length?n:null;return t||r}var rt=function(e){return"string"==typeof e||e instanceof String?o()(e).call(e).replace(/\s/g,"%20"):""},ot=function(e){return se()(rt(e).replace(/%20/g,"_"))},at=function(e){return A()(e).call(e,(function(e,t){return/^x-/.test(t)}))},it=function(e){return A()(e).call(e,(function(e,t){return/^pattern|maxLength|minLength|maximum|minimum/.test(t)}))};function st(e,t){var n,r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:function(){return!0};if("object"!==U()(e)||B()(e)||null===e||!t)return e;var o=_()({},e);return O()(n=C()(o)).call(n,(function(e){e===t&&r(o[e],e)?delete o[e]:o[e]=st(o[e],t,r)})),o}function ut(e){if("string"==typeof e)return e;if(e&&e.toJS&&(e=e.toJS()),"object"===U()(e)&&null!==e)try{return f()(e,null,2)}catch(t){return String(e)}return null==e?"":e.toString()}function ct(e){return"number"==typeof e?e.toString():e}function lt(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{},n=t.returnAll,r=void 0!==n&&n,o=t.allowHashes,a=void 0===o||o;if(!z.a.Map.isMap(e))throw new Error("paramToIdentifier: received a non-Im.Map parameter as input");var i,s,u,c=e.get("name"),l=e.get("in"),p=[];e&&e.hashCode&&l&&c&&a&&p.push(T()(i=T()(s="".concat(l,".")).call(s,c,".hash-")).call(i,e.hashCode()));l&&c&&p.push(T()(u="".concat(l,".")).call(u,c));return p.push(c),r?p:p[0]||""}function pt(e,t){var n,r=lt(e,{returnAll:!0});return A()(n=D()(r).call(r,(function(e){return t[e]}))).call(n,(function(e){return void 0!==e}))[0]}function ft(){return dt(le()(32).toString("base64"))}function ht(e){return dt(fe()("sha256").update(e).digest("base64"))}function dt(e){return e.replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")}var vt=function(e){return!e||!(!de(e)||!e.isEmpty())}}).call(this,n(77).Buffer)},function(e,t,n){e.exports=n(584)},function(e,t,n){var r=n(240);function o(e,t){for(var n=0;n1?t-1:0),r=1;r1&&void 0!==arguments[1]?arguments[1]:r,n=null,a=null;return function(){return o(t,n,arguments)||(a=e.apply(null,arguments)),n=arguments,a}}))},function(e,t,n){var r=n(546),o=n(178);function a(t){return e.exports=a="function"==typeof o&&"symbol"==typeof r?function(e){return typeof e}:function(e){return e&&"function"==typeof o&&e.constructor===o&&e!==o.prototype?"symbol":typeof e},a(t)}e.exports=a},function(e,t,n){e.exports=n(600)},function(e,t,n){e.exports=n(588)},function(e,t,n){e.exports=n(597)},function(e,t,n){"use strict";var r=n(41),o=n(105).f,a=n(354),i=n(34),s=n(108),u=n(70),c=n(52),l=function(e){var t=function(t,n,r){if(this instanceof e){switch(arguments.length){case 0:return new e;case 1:return new e(t);case 2:return new e(t,n)}return new e(t,n,r)}return e.apply(this,arguments)};return t.prototype=e.prototype,t};e.exports=function(e,t){var n,p,f,h,d,v,m,g,y=e.target,b=e.global,_=e.stat,w=e.proto,x=b?r:_?r[y]:(r[y]||{}).prototype,E=b?i:i[y]||(i[y]={}),C=E.prototype;for(f in t)n=!a(b?f:y+(_?".":"#")+f,e.forced)&&x&&c(x,f),d=E[f],n&&(v=e.noTargetGet?(g=o(x,f))&&g.value:x[f]),h=n&&v?v:t[f],n&&typeof d==typeof h||(m=e.bind&&n?s(h,r):e.wrap&&n?l(h):w&&"function"==typeof h?s(Function.call,h):h,(e.sham||h&&h.sham||d&&d.sham)&&u(m,"sham",!0),E[f]=m,w&&(c(i,p=y+"Prototype")||u(i,p,{}),i[p][f]=h,e.real&&C&&!C[f]&&u(C,f,h)))}},function(e,t,n){var r=n(240),o=n(833),a=n(837),i=n(842),s=n(443),u=n(847),c=n(444),l=n(445),p=n(3);function f(e,t){var n=l(e);if(c){var r=c(e);t&&(r=u(r).call(r,(function(t){return s(e,t).enumerable}))),n.push.apply(n,r)}return n}e.exports=function(e){for(var t=1;t>",a={listOf:function(e){return c(e,"List",r.List.isList)},mapOf:function(e,t){return p(e,t,"Map",r.Map.isMap)},orderedMapOf:function(e,t){return p(e,t,"OrderedMap",r.OrderedMap.isOrderedMap)},setOf:function(e){return c(e,"Set",r.Set.isSet)},orderedSetOf:function(e){return c(e,"OrderedSet",r.OrderedSet.isOrderedSet)},stackOf:function(e){return c(e,"Stack",r.Stack.isStack)},iterableOf:function(e){return c(e,"Iterable",r.Iterable.isIterable)},recordOf:function(e){return s((function(t,n,o,a,s){for(var u=arguments.length,c=Array(u>5?u-5:0),l=5;l6?u-6:0),l=6;l5?c-5:0),p=5;p5?i-5:0),u=5;u key("+l[p]+")"].concat(s));if(h instanceof Error)return h}}))}function p(e,t,n,r){return s((function(){for(var o=arguments.length,a=Array(o),i=0;i5?c-5:0),p=5;p4)}function l(e){var t=e.get("swagger");return"string"==typeof t&&i()(t).call(t,"2.0")}function p(e){return function(t,n){return function(r){return n&&n.specSelectors&&n.specSelectors.specJson?c(n.specSelectors.specJson())?u.a.createElement(e,o()({},r,n,{Ori:t})):u.a.createElement(t,r):(console.warn("OAS3 wrapper: couldn't get spec"),null)}}}},function(e,t,n){var r=n(41),o=n(228),a=n(52),i=n(176),s=n(229),u=n(359),c=o("wks"),l=r.Symbol,p=u?l:l&&l.withoutSetter||i;e.exports=function(e){return a(c,e)||(s&&a(l,e)?c[e]=l[e]:c[e]=p("Symbol."+e)),c[e]}},function(e,t,n){"use strict";var r=Object.getOwnPropertySymbols,o=Object.prototype.hasOwnProperty,a=Object.prototype.propertyIsEnumerable;function i(e){if(null==e)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(e)}e.exports=function(){try{if(!Object.assign)return!1;var e=new String("abc");if(e[5]="de","5"===Object.getOwnPropertyNames(e)[0])return!1;for(var t={},n=0;n<10;n++)t["_"+String.fromCharCode(n)]=n;if("0123456789"!==Object.getOwnPropertyNames(t).map((function(e){return t[e]})).join(""))return!1;var r={};return"abcdefghijklmnopqrst".split("").forEach((function(e){r[e]=e})),"abcdefghijklmnopqrst"===Object.keys(Object.assign({},r)).join("")}catch(e){return!1}}()?Object.assign:function(e,t){for(var n,s,u=i(e),c=1;c0){var o=P()(n).call(n,(function(e){return console.error(e),e.line=e.fullPath?g(y,e.fullPath):null,e.path=e.fullPath?e.fullPath.join("."):null,e.level="error",e.type="thrown",e.source="resolver",N()(e,"message",{enumerable:!0,value:e.message}),e}));a.newThrownErrBatch(o)}return r.updateResolved(t)}))}},Ee=[],Ce=Y()(T()(C.a.mark((function e(){var t,n,r,o,a,i,s,u,c,l,p,f,h,d,v,m,g;return C.a.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(t=Ee.system){e.next=4;break}return console.error("debResolveSubtrees: don't have a system to operate on, aborting."),e.abrupt("return");case 4:if(n=t.errActions,r=t.errSelectors,o=t.fn,a=o.resolveSubtree,i=o.AST,s=void 0===i?{}:i,u=t.specSelectors,c=t.specActions,a){e.next=8;break}return console.error("Error: Swagger-Client did not provide a `resolveSubtree` method, doing nothing."),e.abrupt("return");case 8:return l=s.getLineNumberForPath?s.getLineNumberForPath:function(){},p=u.specStr(),f=t.getConfigs(),h=f.modelPropertyMacro,d=f.parameterMacro,v=f.requestInterceptor,m=f.responseInterceptor,e.prev=11,e.next=14,O()(Ee).call(Ee,function(){var e=T()(C.a.mark((function e(t,o){var i,s,c,f,g,y,b;return C.a.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,t;case 2:return i=e.sent,s=i.resultMap,c=i.specWithCurrentSubtrees,e.next=7,a(c,o,{baseDoc:u.url(),modelPropertyMacro:h,parameterMacro:d,requestInterceptor:v,responseInterceptor:m});case 7:return f=e.sent,g=f.errors,y=f.spec,r.allErrors().size&&n.clearBy((function(e){var t;return"thrown"!==e.get("type")||"resolver"!==e.get("source")||!A()(t=e.get("fullPath")).call(t,(function(e,t){return e===o[t]||void 0===o[t]}))})),D()(g)&&g.length>0&&(b=P()(g).call(g,(function(e){return e.line=e.fullPath?l(p,e.fullPath):null,e.path=e.fullPath?e.fullPath.join("."):null,e.level="error",e.type="thrown",e.source="resolver",N()(e,"message",{enumerable:!0,value:e.message}),e})),n.newThrownErrBatch(b)),Z()(s,o,y),Z()(c,o,y),e.abrupt("return",{resultMap:s,specWithCurrentSubtrees:c});case 15:case"end":return e.stop()}}),e)})));return function(t,n){return e.apply(this,arguments)}}(),x.a.resolve({resultMap:(u.specResolvedSubtree([])||Object(q.Map)()).toJS(),specWithCurrentSubtrees:u.specJson().toJS()}));case 14:g=e.sent,delete Ee.system,Ee=[],e.next=22;break;case 19:e.prev=19,e.t0=e.catch(11),console.error(e.t0);case 22:c.updateResolvedSubtree([],g.resultMap);case 23:case"end":return e.stop()}}),e,null,[[11,19]])}))),35),Se=function(e){return function(t){var n;_()(n=P()(Ee).call(Ee,(function(e){return e.join("@@")}))).call(n,e.join("@@"))>-1||(Ee.push(e),Ee.system=t,Ce())}};function Ae(e,t,n,r,o){return{type:ne,payload:{path:e,value:r,paramName:t,paramIn:n,isXml:o}}}function ke(e,t,n,r){return{type:ne,payload:{path:e,param:t,value:n,isXml:r}}}var Oe=function(e,t){return{type:de,payload:{path:e,value:t}}},je=function(){return{type:de,payload:{path:[],value:Object(q.Map)()}}},Te=function(e,t){return{type:oe,payload:{pathMethod:e,isOAS3:t}}},Ie=function(e,t,n,r){return{type:re,payload:{pathMethod:e,paramName:t,paramIn:n,includeEmptyValue:r}}};function Ne(e){return{type:pe,payload:{pathMethod:e}}}function Me(e,t){return{type:fe,payload:{path:e,value:t,key:"consumes_value"}}}function Pe(e,t){return{type:fe,payload:{path:e,value:t,key:"produces_value"}}}var Re=function(e,t,n){return{payload:{path:e,method:t,res:n},type:ae}},De=function(e,t,n){return{payload:{path:e,method:t,req:n},type:ie}},Le=function(e,t,n){return{payload:{path:e,method:t,req:n},type:se}},Be=function(e){return{payload:e,type:ue}},Fe=function(e){return function(t){var n,r,o=t.fn,a=t.specActions,i=t.specSelectors,s=t.getConfigs,c=t.oas3Selectors,p=e.pathName,h=e.method,v=e.operation,g=s(),b=g.requestInterceptor,_=g.responseInterceptor,w=v.toJS();v&&v.get("parameters")&&y()(n=m()(r=v.get("parameters")).call(r,(function(e){return e&&!0===e.get("allowEmptyValue")}))).call(n,(function(t){if(i.parameterInclusionSettingFor([p,h],t.get("name"),t.get("in"))){e.parameters=e.parameters||{};var n=Object(X.C)(t,e.parameters);(!n||n&&0===n.size)&&(e.parameters[t.get("name")]="")}}));if(e.contextUrl=V()(i.url()).toString(),w&&w.operationId?e.operationId=w.operationId:w&&p&&h&&(e.operationId=o.opId(w,p,h)),i.isOAS3()){var x,E=d()(x="".concat(p,":")).call(x,h);e.server=c.selectedServer(E)||c.selectedServer();var S=c.serverVariables({server:e.server,namespace:E}).toJS(),A=c.serverVariables({server:e.server}).toJS();e.serverVariables=f()(S).length?S:A,e.requestContentType=c.requestContentType(p,h),e.responseContentType=c.responseContentType(p,h)||"*/*";var k=c.requestBodyValue(p,h),O=c.requestBodyInclusionSetting(p,h);if(Object(X.t)(k))e.requestBody=JSON.parse(k);else if(k&&k.toJS){var j;e.requestBody=m()(j=P()(k).call(k,(function(e){return q.Map.isMap(e)?e.get("value"):e}))).call(j,(function(e,t){return(D()(e)?0!==e.length:!Object(X.q)(e))||O.get(t)})).toJS()}else e.requestBody=k}var I=l()({},e);I=o.buildRequest(I),a.setRequest(e.pathName,e.method,I);var N=function(){var t=T()(C.a.mark((function t(n){var r,o;return C.a.wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,b.apply(undefined,[n]);case 2:return r=t.sent,o=l()({},r),a.setMutatedRequest(e.pathName,e.method,o),t.abrupt("return",r);case 6:case"end":return t.stop()}}),t)})));return function(e){return t.apply(this,arguments)}}();e.requestInterceptor=N,e.responseInterceptor=_;var M=u()();return o.execute(e).then((function(t){t.duration=u()()-M,a.setResponse(e.pathName,e.method,t)})).catch((function(t){console.error(t),a.setResponse(e.pathName,e.method,{error:!0,err:H()(t)})}))}},Ue=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{},t=e.path,n=e.method,r=i()(e,["path","method"]);return function(e){var a=e.fn.fetch,i=e.specSelectors,s=e.specActions,u=i.specJsonWithResolvedSubtrees().toJS(),c=i.operationScheme(t,n),l=i.contentTypeValues([t,n]).toJS(),p=l.requestContentType,f=l.responseContentType,h=/xml/i.test(p),d=i.parameterValues([t,n],h).toJS();return s.executeRequest(o()(o()({},r),{},{fetch:a,spec:u,pathName:t,method:n,parameters:d,requestContentType:p,scheme:c,responseContentType:f}))}};function qe(e,t){return{type:ce,payload:{path:e,method:t}}}function ze(e,t){return{type:le,payload:{path:e,method:t}}}function Ve(e,t,n){return{type:ve,payload:{scheme:e,path:t,method:n}}}},function(e,t,n){var r=n(36);e.exports=!r((function(){return 7!=Object.defineProperty({},1,{get:function(){return 7}})[1]}))},function(e,t,n){var r=n(34),o=n(52),a=n(227),i=n(63).f;e.exports=function(e){var t=r.Symbol||(r.Symbol={});o(t,e)||i(t,e,{value:a.f(e)})}},function(e,t,n){"use strict";var r=n(162),o=["kind","resolve","construct","instanceOf","predicate","represent","defaultStyle","styleAliases"],a=["scalar","sequence","mapping"];e.exports=function(e,t){var n,i;if(t=t||{},Object.keys(t).forEach((function(t){if(-1===o.indexOf(t))throw new r('Unknown option "'+t+'" is met in definition of "'+e+'" YAML type.')})),this.tag=e,this.kind=t.kind||null,this.resolve=t.resolve||function(){return!0},this.construct=t.construct||function(e){return e},this.instanceOf=t.instanceOf||null,this.predicate=t.predicate||null,this.represent=t.represent||null,this.defaultStyle=t.defaultStyle||null,this.styleAliases=(n=t.styleAliases||null,i={},null!==n&&Object.keys(n).forEach((function(e){n[e].forEach((function(t){i[String(t)]=e}))})),i),-1===a.indexOf(this.kind))throw new r('Unknown kind "'+this.kind+'" is specified for "'+e+'" YAML type.')}},function(e,t,n){var r=n(399),o=n(241),a=n(673),i=n(178),s=n(181);e.exports=function(e,t){var n;if(void 0===i||null==a(e)){if(o(e)||(n=s(e))||t&&e&&"number"==typeof e.length){n&&(e=n);var u=0,c=function(){};return{s:c,n:function(){return u>=e.length?{done:!0}:{done:!1,value:e[u++]}},e:function(e){throw e},f:c}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var l,p=!0,f=!1;return{s:function(){n=r(e)},n:function(){var e=n.next();return p=e.done,e},e:function(e){f=!0,l=e},f:function(){try{p||null==n.return||n.return()}finally{if(f)throw l}}}}},function(e,t){var n={}.hasOwnProperty;e.exports=function(e,t){return n.call(e,t)}},function(e,t){var n=Array.isArray;e.exports=n},function(e,t){var n;n=function(){return this}();try{n=n||new Function("return this")()}catch(e){"object"==typeof window&&(n=window)}e.exports=n},function(e,t,n){var r=n(45);e.exports=function(e){if(!r(e))throw TypeError(String(e)+" is not an object");return e}},function(e,t,n){var r=n(446),o=n(444),a=n(853);e.exports=function(e,t){if(null==e)return{};var n,i,s=a(e,t);if(o){var u=o(e);for(i=0;i=0||Object.prototype.propertyIsEnumerable.call(e,n)&&(s[n]=e[n])}return s}},function(e,t,n){"use strict";var r=!("undefined"==typeof window||!window.document||!window.document.createElement),o={canUseDOM:r,canUseWorkers:"undefined"!=typeof Worker,canUseEventListeners:r&&!(!window.addEventListener&&!window.attachEvent),canUseViewport:r&&!!window.screen,isInWorker:!r};e.exports=o},function(e,t,n){e.exports=n(637)},function(e,t,n){"use strict";n.r(t),n.d(t,"UPDATE_SELECTED_SERVER",(function(){return r})),n.d(t,"UPDATE_REQUEST_BODY_VALUE",(function(){return o})),n.d(t,"UPDATE_REQUEST_BODY_INCLUSION",(function(){return a})),n.d(t,"UPDATE_ACTIVE_EXAMPLES_MEMBER",(function(){return i})),n.d(t,"UPDATE_REQUEST_CONTENT_TYPE",(function(){return s})),n.d(t,"UPDATE_RESPONSE_CONTENT_TYPE",(function(){return u})),n.d(t,"UPDATE_SERVER_VARIABLE_VALUE",(function(){return c})),n.d(t,"SET_REQUEST_BODY_VALIDATE_ERROR",(function(){return l})),n.d(t,"CLEAR_REQUEST_BODY_VALIDATE_ERROR",(function(){return p})),n.d(t,"CLEAR_REQUEST_BODY_VALUE",(function(){return f})),n.d(t,"setSelectedServer",(function(){return h})),n.d(t,"setRequestBodyValue",(function(){return d})),n.d(t,"setRequestBodyInclusion",(function(){return v})),n.d(t,"setActiveExamplesMember",(function(){return m})),n.d(t,"setRequestContentType",(function(){return g})),n.d(t,"setResponseContentType",(function(){return y})),n.d(t,"setServerVariableValue",(function(){return b})),n.d(t,"setRequestBodyValidateError",(function(){return _})),n.d(t,"clearRequestBodyValidateError",(function(){return w})),n.d(t,"initRequestBodyValidateError",(function(){return x})),n.d(t,"clearRequestBodyValue",(function(){return E}));var r="oas3_set_servers",o="oas3_set_request_body_value",a="oas3_set_request_body_inclusion",i="oas3_set_active_examples_member",s="oas3_set_request_content_type",u="oas3_set_response_content_type",c="oas3_set_server_variable_value",l="oas3_set_request_body_validate_error",p="oas3_clear_request_body_validate_error",f="oas3_clear_request_body_value";function h(e,t){return{type:r,payload:{selectedServerUrl:e,namespace:t}}}function d(e){var t=e.value,n=e.pathMethod;return{type:o,payload:{value:t,pathMethod:n}}}function v(e){var t=e.value,n=e.pathMethod,r=e.name;return{type:a,payload:{value:t,pathMethod:n,name:r}}}function m(e){var t=e.name,n=e.pathMethod,r=e.contextType,o=e.contextName;return{type:i,payload:{name:t,pathMethod:n,contextType:r,contextName:o}}}function g(e){var t=e.value,n=e.pathMethod;return{type:s,payload:{value:t,pathMethod:n}}}function y(e){var t=e.value,n=e.path,r=e.method;return{type:u,payload:{value:t,path:n,method:r}}}function b(e){var t=e.server,n=e.namespace,r=e.key,o=e.val;return{type:c,payload:{server:t,namespace:n,key:r,val:o}}}var _=function(e){var t=e.path,n=e.method,r=e.validationErrors;return{type:l,payload:{path:t,method:n,validationErrors:r}}},w=function(e){var t=e.path,n=e.method;return{type:p,payload:{path:t,method:n}}},x=function(e){var t=e.pathMethod;return{type:p,payload:{path:t[0],method:t[1]}}},E=function(e){var t=e.pathMethod;return{type:f,payload:{pathMethod:t}}}},function(e,t){e.exports=function(e){var t=typeof e;return null!=e&&("object"==t||"function"==t)}},function(e,t,n){"use strict";n.d(t,"b",(function(){return b})),n.d(t,"e",(function(){return _})),n.d(t,"c",(function(){return x})),n.d(t,"a",(function(){return E})),n.d(t,"d",(function(){return C}));var r=n(51),o=n.n(r),a=n(16),i=n.n(a),s=n(35),u=n.n(s),c=n(2),l=n.n(c),p=n(20),f=n.n(p),h=n(60),d=n.n(h),v=n(349),m=n.n(v),g=function(e){return String.prototype.toLowerCase.call(e)},y=function(e){return e.replace(/[^\w]/gi,"_")};function b(e){var t=e.openapi;return!!t&&m()(t,"3")}function _(e,t){var n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:"",r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:{},o=r.v2OperationIdCompatibilityMode;if(!e||"object"!==f()(e))return null;var a=(e.operationId||"").replace(/\s/g,"");return a.length?y(e.operationId):w(t,n,{v2OperationIdCompatibilityMode:o})}function w(e,t){var n,r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},o=r.v2OperationIdCompatibilityMode;if(o){var a,i,s=l()(a="".concat(t.toLowerCase(),"_")).call(a,e).replace(/[\s!@#$%^&*()_+=[{\]};:<>|./?,\\'""-]/g,"_");return(s=s||l()(i="".concat(e.substring(1),"_")).call(i,t)).replace(/((_){2,})/g,"_").replace(/^(_)*/g,"").replace(/([_])*$/g,"")}return l()(n="".concat(g(t))).call(n,y(e))}function x(e,t){var n;return l()(n="".concat(g(t),"-")).call(n,e)}function E(e,t){return e&&e.paths?function(e,t){return function(e,t,n){if(!e||"object"!==f()(e)||!e.paths||"object"!==f()(e.paths))return null;var r=e.paths;for(var o in r)for(var a in r[o])if("PARAMETERS"!==a.toUpperCase()){var i=r[o][a];if(i&&"object"===f()(i)){var s={spec:e,pathName:o,method:a.toUpperCase(),operation:i},u=t(s);if(n&&u)return s}}return}(e,t,!0)||null}(e,(function(e){var n,r=e.pathName,o=e.method,a=e.operation;if(!a||"object"!==f()(a))return!1;var i=a.operationId,s=_(a,r,o),c=x(r,o);return u()(n=[s,c,i]).call(n,(function(e){return e&&e===t}))})):null}function C(e){var t=e.spec,n=t.paths,r={};if(!n||t.$$normalized)return e;for(var a in n){var s=n[a];if(d()(s)){var c=s.parameters,p=function(e){var n=s[e];if(!d()(n))return"continue";var p=_(n,a,e);if(p){r[p]?r[p].push(n):r[p]=[n];var f=r[p];if(f.length>1)i()(f).call(f,(function(e,t){var n;e.__originalOperationId=e.__originalOperationId||e.operationId,e.operationId=l()(n="".concat(p)).call(n,t+1)}));else if(void 0!==n.operationId){var h=f[0];h.__originalOperationId=h.__originalOperationId||n.operationId,h.operationId=p}}if("parameters"!==e){var v=[],m={};for(var g in t)"produces"!==g&&"consumes"!==g&&"security"!==g||(m[g]=t[g],v.push(m));if(c&&(m.parameters=c,v.push(m)),v.length){var y,b=o()(v);try{for(b.s();!(y=b.n()).done;){var w=y.value;for(var x in w)if(n[x]){if("parameters"===x){var E,C=o()(w[x]);try{var S=function(){var e,t=E.value;u()(e=n[x]).call(e,(function(e){return e.name&&e.name===t.name||e.$ref&&e.$ref===t.$ref||e.$$ref&&e.$$ref===t.$$ref||e===t}))||n[x].push(t)};for(C.s();!(E=C.n()).done;)S()}catch(e){C.e(e)}finally{C.f()}}}else n[x]=w[x]}}catch(e){b.e(e)}finally{b.f()}}}};for(var f in s)p(f)}}return t.$$normalized=!0,e}},function(e,t,n){"use strict";n.r(t),n.d(t,"NEW_THROWN_ERR",(function(){return a})),n.d(t,"NEW_THROWN_ERR_BATCH",(function(){return i})),n.d(t,"NEW_SPEC_ERR",(function(){return s})),n.d(t,"NEW_SPEC_ERR_BATCH",(function(){return u})),n.d(t,"NEW_AUTH_ERR",(function(){return c})),n.d(t,"CLEAR",(function(){return l})),n.d(t,"CLEAR_BY",(function(){return p})),n.d(t,"newThrownErr",(function(){return f})),n.d(t,"newThrownErrBatch",(function(){return h})),n.d(t,"newSpecErr",(function(){return d})),n.d(t,"newSpecErrBatch",(function(){return v})),n.d(t,"newAuthErr",(function(){return m})),n.d(t,"clear",(function(){return g})),n.d(t,"clearBy",(function(){return y}));var r=n(141),o=n.n(r),a="err_new_thrown_err",i="err_new_thrown_err_batch",s="err_new_spec_err",u="err_new_spec_err_batch",c="err_new_auth_err",l="err_clear",p="err_clear_by";function f(e){return{type:a,payload:o()(e)}}function h(e){return{type:i,payload:e}}function d(e){return{type:s,payload:e}}function v(e){return{type:u,payload:e}}function m(e){return{type:c,payload:e}}function g(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};return{type:l,payload:e}}function y(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:function(){return!0};return{type:p,payload:e}}},function(e,t,n){var r=n(48),o=n(353),a=n(55),i=n(175),s=Object.defineProperty;t.f=r?s:function(e,t,n){if(a(e),t=i(t,!0),a(n),o)try{return s(e,t,n)}catch(e){}if("get"in n||"set"in n)throw TypeError("Accessors not supported");return"value"in n&&(e[t]=n.value),e}},function(e,t,n){var r=n(48),o=n(36),a=n(52),i=Object.defineProperty,s={},u=function(e){throw e};e.exports=function(e,t){if(a(s,e))return s[e];t||(t={});var n=[][e],c=!!a(t,"ACCESSORS")&&t.ACCESSORS,l=a(t,0)?t[0]:u,p=a(t,1)?t[1]:void 0;return s[e]=!!n&&!o((function(){if(c&&!r)return!0;var e={length:-1};c?i(e,1,{enumerable:!0,get:u}):e[1]=1,n.call(e,l,p)}))}},function(e,t){"function"==typeof Object.create?e.exports=function(e,t){e.super_=t,e.prototype=Object.create(t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}})}:e.exports=function(e,t){e.super_=t;var n=function(){};n.prototype=t.prototype,e.prototype=new n,e.prototype.constructor=e}},function(e,t,n){var r=n(77),o=r.Buffer;function a(e,t){for(var n in e)t[n]=e[n]}function i(e,t,n){return o(e,t,n)}o.from&&o.alloc&&o.allocUnsafe&&o.allocUnsafeSlow?e.exports=r:(a(r,t),t.Buffer=i),a(o,i),i.from=function(e,t,n){if("number"==typeof e)throw new TypeError("Argument must not be a number");return o(e,t,n)},i.alloc=function(e,t,n){if("number"!=typeof e)throw new TypeError("Argument must be a number");var r=o(e);return void 0!==t?"string"==typeof n?r.fill(t,n):r.fill(t):r.fill(0),r},i.allocUnsafe=function(e){if("number"!=typeof e)throw new TypeError("Argument must be a number");return o(e)},i.allocUnsafeSlow=function(e){if("number"!=typeof e)throw new TypeError("Argument must be a number");return r.SlowBuffer(e)}},function(e,t,n){var r;!function(){"use strict";var n={}.hasOwnProperty;function o(){for(var e=[],t=0;t=i())throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+i().toString(16)+" bytes");return 0|e}function d(e,t){if(u.isBuffer(e))return e.length;if("undefined"!=typeof ArrayBuffer&&"function"==typeof ArrayBuffer.isView&&(ArrayBuffer.isView(e)||e instanceof ArrayBuffer))return e.byteLength;"string"!=typeof e&&(e=""+e);var n=e.length;if(0===n)return 0;for(var r=!1;;)switch(t){case"ascii":case"latin1":case"binary":return n;case"utf8":case"utf-8":case void 0:return q(e).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*n;case"hex":return n>>>1;case"base64":return z(e).length;default:if(r)return q(e).length;t=(""+t).toLowerCase(),r=!0}}function v(e,t,n){var r=!1;if((void 0===t||t<0)&&(t=0),t>this.length)return"";if((void 0===n||n>this.length)&&(n=this.length),n<=0)return"";if((n>>>=0)<=(t>>>=0))return"";for(e||(e="utf8");;)switch(e){case"hex":return T(this,t,n);case"utf8":case"utf-8":return A(this,t,n);case"ascii":return O(this,t,n);case"latin1":case"binary":return j(this,t,n);case"base64":return S(this,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return I(this,t,n);default:if(r)throw new TypeError("Unknown encoding: "+e);e=(e+"").toLowerCase(),r=!0}}function m(e,t,n){var r=e[t];e[t]=e[n],e[n]=r}function g(e,t,n,r,o){if(0===e.length)return-1;if("string"==typeof n?(r=n,n=0):n>2147483647?n=2147483647:n<-2147483648&&(n=-2147483648),n=+n,isNaN(n)&&(n=o?0:e.length-1),n<0&&(n=e.length+n),n>=e.length){if(o)return-1;n=e.length-1}else if(n<0){if(!o)return-1;n=0}if("string"==typeof t&&(t=u.from(t,r)),u.isBuffer(t))return 0===t.length?-1:y(e,t,n,r,o);if("number"==typeof t)return t&=255,u.TYPED_ARRAY_SUPPORT&&"function"==typeof Uint8Array.prototype.indexOf?o?Uint8Array.prototype.indexOf.call(e,t,n):Uint8Array.prototype.lastIndexOf.call(e,t,n):y(e,[t],n,r,o);throw new TypeError("val must be string, number or Buffer")}function y(e,t,n,r,o){var a,i=1,s=e.length,u=t.length;if(void 0!==r&&("ucs2"===(r=String(r).toLowerCase())||"ucs-2"===r||"utf16le"===r||"utf-16le"===r)){if(e.length<2||t.length<2)return-1;i=2,s/=2,u/=2,n/=2}function c(e,t){return 1===i?e[t]:e.readUInt16BE(t*i)}if(o){var l=-1;for(a=n;as&&(n=s-u),a=n;a>=0;a--){for(var p=!0,f=0;fo&&(r=o):r=o;var a=t.length;if(a%2!=0)throw new TypeError("Invalid hex string");r>a/2&&(r=a/2);for(var i=0;i>8,o=n%256,a.push(o),a.push(r);return a}(t,e.length-n),e,n,r)}function S(e,t,n){return 0===t&&n===e.length?r.fromByteArray(e):r.fromByteArray(e.slice(t,n))}function A(e,t,n){n=Math.min(e.length,n);for(var r=[],o=t;o239?4:c>223?3:c>191?2:1;if(o+p<=n)switch(p){case 1:c<128&&(l=c);break;case 2:128==(192&(a=e[o+1]))&&(u=(31&c)<<6|63&a)>127&&(l=u);break;case 3:a=e[o+1],i=e[o+2],128==(192&a)&&128==(192&i)&&(u=(15&c)<<12|(63&a)<<6|63&i)>2047&&(u<55296||u>57343)&&(l=u);break;case 4:a=e[o+1],i=e[o+2],s=e[o+3],128==(192&a)&&128==(192&i)&&128==(192&s)&&(u=(15&c)<<18|(63&a)<<12|(63&i)<<6|63&s)>65535&&u<1114112&&(l=u)}null===l?(l=65533,p=1):l>65535&&(l-=65536,r.push(l>>>10&1023|55296),l=56320|1023&l),r.push(l),o+=p}return function(e){var t=e.length;if(t<=k)return String.fromCharCode.apply(String,e);var n="",r=0;for(;r0&&(e=this.toString("hex",0,n).match(/.{2}/g).join(" "),this.length>n&&(e+=" ... ")),""},u.prototype.compare=function(e,t,n,r,o){if(!u.isBuffer(e))throw new TypeError("Argument must be a Buffer");if(void 0===t&&(t=0),void 0===n&&(n=e?e.length:0),void 0===r&&(r=0),void 0===o&&(o=this.length),t<0||n>e.length||r<0||o>this.length)throw new RangeError("out of range index");if(r>=o&&t>=n)return 0;if(r>=o)return-1;if(t>=n)return 1;if(this===e)return 0;for(var a=(o>>>=0)-(r>>>=0),i=(n>>>=0)-(t>>>=0),s=Math.min(a,i),c=this.slice(r,o),l=e.slice(t,n),p=0;po)&&(n=o),e.length>0&&(n<0||t<0)||t>this.length)throw new RangeError("Attempt to write outside buffer bounds");r||(r="utf8");for(var a=!1;;)switch(r){case"hex":return b(this,e,t,n);case"utf8":case"utf-8":return _(this,e,t,n);case"ascii":return w(this,e,t,n);case"latin1":case"binary":return x(this,e,t,n);case"base64":return E(this,e,t,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return C(this,e,t,n);default:if(a)throw new TypeError("Unknown encoding: "+r);r=(""+r).toLowerCase(),a=!0}},u.prototype.toJSON=function(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};var k=4096;function O(e,t,n){var r="";n=Math.min(e.length,n);for(var o=t;or)&&(n=r);for(var o="",a=t;an)throw new RangeError("Trying to access beyond buffer length")}function M(e,t,n,r,o,a){if(!u.isBuffer(e))throw new TypeError('"buffer" argument must be a Buffer instance');if(t>o||te.length)throw new RangeError("Index out of range")}function P(e,t,n,r){t<0&&(t=65535+t+1);for(var o=0,a=Math.min(e.length-n,2);o>>8*(r?o:1-o)}function R(e,t,n,r){t<0&&(t=4294967295+t+1);for(var o=0,a=Math.min(e.length-n,4);o>>8*(r?o:3-o)&255}function D(e,t,n,r,o,a){if(n+r>e.length)throw new RangeError("Index out of range");if(n<0)throw new RangeError("Index out of range")}function L(e,t,n,r,a){return a||D(e,0,n,4),o.write(e,t,n,r,23,4),n+4}function B(e,t,n,r,a){return a||D(e,0,n,8),o.write(e,t,n,r,52,8),n+8}u.prototype.slice=function(e,t){var n,r=this.length;if((e=~~e)<0?(e+=r)<0&&(e=0):e>r&&(e=r),(t=void 0===t?r:~~t)<0?(t+=r)<0&&(t=0):t>r&&(t=r),t0&&(o*=256);)r+=this[e+--t]*o;return r},u.prototype.readUInt8=function(e,t){return t||N(e,1,this.length),this[e]},u.prototype.readUInt16LE=function(e,t){return t||N(e,2,this.length),this[e]|this[e+1]<<8},u.prototype.readUInt16BE=function(e,t){return t||N(e,2,this.length),this[e]<<8|this[e+1]},u.prototype.readUInt32LE=function(e,t){return t||N(e,4,this.length),(this[e]|this[e+1]<<8|this[e+2]<<16)+16777216*this[e+3]},u.prototype.readUInt32BE=function(e,t){return t||N(e,4,this.length),16777216*this[e]+(this[e+1]<<16|this[e+2]<<8|this[e+3])},u.prototype.readIntLE=function(e,t,n){e|=0,t|=0,n||N(e,t,this.length);for(var r=this[e],o=1,a=0;++a=(o*=128)&&(r-=Math.pow(2,8*t)),r},u.prototype.readIntBE=function(e,t,n){e|=0,t|=0,n||N(e,t,this.length);for(var r=t,o=1,a=this[e+--r];r>0&&(o*=256);)a+=this[e+--r]*o;return a>=(o*=128)&&(a-=Math.pow(2,8*t)),a},u.prototype.readInt8=function(e,t){return t||N(e,1,this.length),128&this[e]?-1*(255-this[e]+1):this[e]},u.prototype.readInt16LE=function(e,t){t||N(e,2,this.length);var n=this[e]|this[e+1]<<8;return 32768&n?4294901760|n:n},u.prototype.readInt16BE=function(e,t){t||N(e,2,this.length);var n=this[e+1]|this[e]<<8;return 32768&n?4294901760|n:n},u.prototype.readInt32LE=function(e,t){return t||N(e,4,this.length),this[e]|this[e+1]<<8|this[e+2]<<16|this[e+3]<<24},u.prototype.readInt32BE=function(e,t){return t||N(e,4,this.length),this[e]<<24|this[e+1]<<16|this[e+2]<<8|this[e+3]},u.prototype.readFloatLE=function(e,t){return t||N(e,4,this.length),o.read(this,e,!0,23,4)},u.prototype.readFloatBE=function(e,t){return t||N(e,4,this.length),o.read(this,e,!1,23,4)},u.prototype.readDoubleLE=function(e,t){return t||N(e,8,this.length),o.read(this,e,!0,52,8)},u.prototype.readDoubleBE=function(e,t){return t||N(e,8,this.length),o.read(this,e,!1,52,8)},u.prototype.writeUIntLE=function(e,t,n,r){(e=+e,t|=0,n|=0,r)||M(this,e,t,n,Math.pow(2,8*n)-1,0);var o=1,a=0;for(this[t]=255&e;++a=0&&(a*=256);)this[t+o]=e/a&255;return t+n},u.prototype.writeUInt8=function(e,t,n){return e=+e,t|=0,n||M(this,e,t,1,255,0),u.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),this[t]=255&e,t+1},u.prototype.writeUInt16LE=function(e,t,n){return e=+e,t|=0,n||M(this,e,t,2,65535,0),u.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8):P(this,e,t,!0),t+2},u.prototype.writeUInt16BE=function(e,t,n){return e=+e,t|=0,n||M(this,e,t,2,65535,0),u.TYPED_ARRAY_SUPPORT?(this[t]=e>>>8,this[t+1]=255&e):P(this,e,t,!1),t+2},u.prototype.writeUInt32LE=function(e,t,n){return e=+e,t|=0,n||M(this,e,t,4,4294967295,0),u.TYPED_ARRAY_SUPPORT?(this[t+3]=e>>>24,this[t+2]=e>>>16,this[t+1]=e>>>8,this[t]=255&e):R(this,e,t,!0),t+4},u.prototype.writeUInt32BE=function(e,t,n){return e=+e,t|=0,n||M(this,e,t,4,4294967295,0),u.TYPED_ARRAY_SUPPORT?(this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e):R(this,e,t,!1),t+4},u.prototype.writeIntLE=function(e,t,n,r){if(e=+e,t|=0,!r){var o=Math.pow(2,8*n-1);M(this,e,t,n,o-1,-o)}var a=0,i=1,s=0;for(this[t]=255&e;++a>0)-s&255;return t+n},u.prototype.writeIntBE=function(e,t,n,r){if(e=+e,t|=0,!r){var o=Math.pow(2,8*n-1);M(this,e,t,n,o-1,-o)}var a=n-1,i=1,s=0;for(this[t+a]=255&e;--a>=0&&(i*=256);)e<0&&0===s&&0!==this[t+a+1]&&(s=1),this[t+a]=(e/i>>0)-s&255;return t+n},u.prototype.writeInt8=function(e,t,n){return e=+e,t|=0,n||M(this,e,t,1,127,-128),u.TYPED_ARRAY_SUPPORT||(e=Math.floor(e)),e<0&&(e=255+e+1),this[t]=255&e,t+1},u.prototype.writeInt16LE=function(e,t,n){return e=+e,t|=0,n||M(this,e,t,2,32767,-32768),u.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8):P(this,e,t,!0),t+2},u.prototype.writeInt16BE=function(e,t,n){return e=+e,t|=0,n||M(this,e,t,2,32767,-32768),u.TYPED_ARRAY_SUPPORT?(this[t]=e>>>8,this[t+1]=255&e):P(this,e,t,!1),t+2},u.prototype.writeInt32LE=function(e,t,n){return e=+e,t|=0,n||M(this,e,t,4,2147483647,-2147483648),u.TYPED_ARRAY_SUPPORT?(this[t]=255&e,this[t+1]=e>>>8,this[t+2]=e>>>16,this[t+3]=e>>>24):R(this,e,t,!0),t+4},u.prototype.writeInt32BE=function(e,t,n){return e=+e,t|=0,n||M(this,e,t,4,2147483647,-2147483648),e<0&&(e=4294967295+e+1),u.TYPED_ARRAY_SUPPORT?(this[t]=e>>>24,this[t+1]=e>>>16,this[t+2]=e>>>8,this[t+3]=255&e):R(this,e,t,!1),t+4},u.prototype.writeFloatLE=function(e,t,n){return L(this,e,t,!0,n)},u.prototype.writeFloatBE=function(e,t,n){return L(this,e,t,!1,n)},u.prototype.writeDoubleLE=function(e,t,n){return B(this,e,t,!0,n)},u.prototype.writeDoubleBE=function(e,t,n){return B(this,e,t,!1,n)},u.prototype.copy=function(e,t,n,r){if(n||(n=0),r||0===r||(r=this.length),t>=e.length&&(t=e.length),t||(t=0),r>0&&r=this.length)throw new RangeError("sourceStart out of bounds");if(r<0)throw new RangeError("sourceEnd out of bounds");r>this.length&&(r=this.length),e.length-t=0;--o)e[o+t]=this[o+n];else if(a<1e3||!u.TYPED_ARRAY_SUPPORT)for(o=0;o>>=0,n=void 0===n?this.length:n>>>0,e||(e=0),"number"==typeof e)for(a=t;a55295&&n<57344){if(!o){if(n>56319){(t-=3)>-1&&a.push(239,191,189);continue}if(i+1===r){(t-=3)>-1&&a.push(239,191,189);continue}o=n;continue}if(n<56320){(t-=3)>-1&&a.push(239,191,189),o=n;continue}n=65536+(o-55296<<10|n-56320)}else o&&(t-=3)>-1&&a.push(239,191,189);if(o=null,n<128){if((t-=1)<0)break;a.push(n)}else if(n<2048){if((t-=2)<0)break;a.push(n>>6|192,63&n|128)}else if(n<65536){if((t-=3)<0)break;a.push(n>>12|224,n>>6&63|128,63&n|128)}else{if(!(n<1114112))throw new Error("Invalid code point");if((t-=4)<0)break;a.push(n>>18|240,n>>12&63|128,n>>6&63|128,63&n|128)}}return a}function z(e){return r.toByteArray(function(e){if((e=function(e){return e.trim?e.trim():e.replace(/^\s+|\s+$/g,"")}(e).replace(F,"")).length<2)return"";for(;e.length%4!=0;)e+="=";return e}(e))}function V(e,t,n,r){for(var o=0;o=t.length||o>=e.length);++o)t[o+n]=e[o];return o}}).call(this,n(54))},function(e,t,n){e.exports=n(857)},function(e,t){e.exports=function(e){if("function"!=typeof e)throw TypeError(String(e)+" is not a function");return e}},function(e,t,n){var r=n(148),o=Math.min;e.exports=function(e){return e>0?o(r(e),9007199254740991):0}},function(e,t,n){"use strict";function r(e){return function(){return e}}var o=function(){};o.thatReturns=r,o.thatReturnsFalse=r(!1),o.thatReturnsTrue=r(!0),o.thatReturnsNull=r(null),o.thatReturnsThis=function(){return this},o.thatReturnsArgument=function(e){return e},e.exports=o},function(e,t,n){"use strict";var r=n(31),o=n(40),a=n(467),i=n(122),s=n(468),u=n(138),c=n(199),l=n(26),p=[],f=0,h=a.getPooled(),d=!1,v=null;function m(){x.ReactReconcileTransaction&&v||r("123")}var g=[{initialize:function(){this.dirtyComponentsLength=p.length},close:function(){this.dirtyComponentsLength!==p.length?(p.splice(0,this.dirtyComponentsLength),w()):p.length=0}},{initialize:function(){this.callbackQueue.reset()},close:function(){this.callbackQueue.notifyAll()}}];function y(){this.reinitializeTransaction(),this.dirtyComponentsLength=null,this.callbackQueue=a.getPooled(),this.reconcileTransaction=x.ReactReconcileTransaction.getPooled(!0)}function b(e,t){return e._mountOrder-t._mountOrder}function _(e){var t=e.dirtyComponentsLength;t!==p.length&&r("124",t,p.length),p.sort(b),f++;for(var n=0;nE;E++)if((f||E in _)&&(y=w(g=_[E],E,b),e))if(t)S[E]=y;else if(y)switch(e){case 3:return!0;case 5:return g;case 6:return E;case 2:u.call(S,g)}else if(l)return!1;return p?-1:c||l?l:S}};e.exports={forEach:c(0),map:c(1),filter:c(2),some:c(3),every:c(4),find:c(5),findIndex:c(6)}},function(e,t,n){"use strict";e.exports={current:null}},function(e,t){e.exports=function(e){return null!=e&&"object"==typeof e}},function(e,t){var n,r,o=e.exports={};function a(){throw new Error("setTimeout has not been defined")}function i(){throw new Error("clearTimeout has not been defined")}function s(e){if(n===setTimeout)return setTimeout(e,0);if((n===a||!n)&&setTimeout)return n=setTimeout,setTimeout(e,0);try{return n(e,0)}catch(t){try{return n.call(null,e,0)}catch(t){return n.call(this,e,0)}}}!function(){try{n="function"==typeof setTimeout?setTimeout:a}catch(e){n=a}try{r="function"==typeof clearTimeout?clearTimeout:i}catch(e){r=i}}();var u,c=[],l=!1,p=-1;function f(){l&&u&&(l=!1,u.length?c=u.concat(c):p=-1,c.length&&h())}function h(){if(!l){var e=s(f);l=!0;for(var t=c.length;t;){for(u=c,c=[];++p1)for(var n=1;n0&&"/"!==t[0]}));function Ce(e,t,n){var r;t=t||[];var o=we.apply(void 0,A()(r=[e]).call(r,O()(t))).get("parameters",Object(I.List)());return f()(o).call(o,(function(e,t){var r=n&&"body"===t.get("in")?t.get("value_xml"):t.get("value");return e.set(Object(T.B)(t,{allowHashes:!1}),r)}),Object(I.fromJS)({}))}function Se(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";if(I.List.isList(e))return u()(e).call(e,(function(e){return I.Map.isMap(e)&&e.get("in")===t}))}function Ae(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";if(I.List.isList(e))return u()(e).call(e,(function(e){return I.Map.isMap(e)&&e.get("type")===t}))}function ke(e,t){var n,r;t=t||[];var o=z(e).getIn(A()(n=["paths"]).call(n,O()(t)),Object(I.fromJS)({})),a=e.getIn(A()(r=["meta","paths"]).call(r,O()(t)),Object(I.fromJS)({})),i=Oe(e,t),s=o.get("parameters")||new I.List,u=a.get("consumes_value")?a.get("consumes_value"):Ae(s,"file")?"multipart/form-data":Ae(s,"formData")?"application/x-www-form-urlencoded":void 0;return Object(I.fromJS)({requestContentType:u,responseContentType:i})}function Oe(e,t){var n,r;t=t||[];var o=z(e).getIn(A()(n=["paths"]).call(n,O()(t)),null);if(null!==o){var a=e.getIn(A()(r=["meta","paths"]).call(r,O()(t),["produces_value"]),null),i=o.getIn(["produces",0],null);return a||i||"application/json"}}function je(e,t){var n;t=t||[];var r=z(e),o=r.getIn(A()(n=["paths"]).call(n,O()(t)),null);if(null!==o){var a=t,s=i()(a,1)[0],u=o.get("produces",null),c=r.getIn(["paths",s,"produces"],null),l=r.getIn(["produces"],null);return u||c||l}}function Te(e,t){var n;t=t||[];var r=z(e),o=r.getIn(A()(n=["paths"]).call(n,O()(t)),null);if(null!==o){var a=t,s=i()(a,1)[0],u=o.get("consumes",null),c=r.getIn(["paths",s,"consumes"],null),l=r.getIn(["consumes"],null);return u||c||l}}var Ie=function(e,t,n){var r=e.get("url").match(/^([a-z][a-z0-9+\-.]*):/),a=o()(r)?r[1]:null;return e.getIn(["scheme",t,n])||e.getIn(["scheme","_defaultScheme"])||a||""},Ne=function(e,t,n){var r;return _()(r=["http","https"]).call(r,Ie(e,t,n))>-1},Me=function(e,t){var n;t=t||[];var r=e.getIn(A()(n=["meta","paths"]).call(n,O()(t),["parameters"]),Object(I.fromJS)([])),o=!0;return x()(r).call(r,(function(e){var t=e.get("errors");t&&t.count()&&(o=!1)})),o},Pe=function(e,t){var n,r,o={requestBody:!1,requestContentType:{}},a=e.getIn(A()(n=["resolvedSubtrees","paths"]).call(n,O()(t),["requestBody"]),Object(I.fromJS)([]));return a.size<1||(a.getIn(["required"])&&(o.requestBody=a.getIn(["required"])),x()(r=a.getIn(["content"]).entrySeq()).call(r,(function(e){var t=e[0];if(e[1].getIn(["schema","required"])){var n=e[1].getIn(["schema","required"]).toJS();o.requestContentType[t]=n}}))),o},Re=function(e,t,n,r){var o,a=e.getIn(A()(o=["resolvedSubtrees","paths"]).call(o,O()(t),["requestBody","content"]),Object(I.fromJS)([]));if(a.size<2||!n||!r)return!1;var i=a.getIn([n,"schema","properties"],Object(I.fromJS)([])),s=a.getIn([r,"schema","properties"],Object(I.fromJS)([]));return!!i.equals(s)};function De(e){return I.Map.isMap(e)?e:new I.Map}},function(e,t,n){"use strict";(function(t){var r=n(891),o=n(892),a=/^[A-Za-z][A-Za-z0-9+-.]*:\/\//,i=/^([a-z][a-z0-9.+-]*:)?(\/\/)?([\S\s]*)/i,s=new RegExp("^[\\x09\\x0A\\x0B\\x0C\\x0D\\x20\\xA0\\u1680\\u180E\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200A\\u202F\\u205F\\u3000\\u2028\\u2029\\uFEFF]+");function u(e){return(e||"").toString().replace(s,"")}var c=[["#","hash"],["?","query"],function(e){return e.replace("\\","/")},["/","pathname"],["@","auth",1],[NaN,"host",void 0,1,1],[/:(\d+)$/,"port",void 0,1],[NaN,"hostname",void 0,1,1]],l={hash:1,query:1};function p(e){var n,r=("undefined"!=typeof window?window:void 0!==t?t:"undefined"!=typeof self?self:{}).location||{},o={},i=typeof(e=e||r);if("blob:"===e.protocol)o=new h(unescape(e.pathname),{});else if("string"===i)for(n in o=new h(e,{}),l)delete o[n];else if("object"===i){for(n in e)n in l||(o[n]=e[n]);void 0===o.slashes&&(o.slashes=a.test(e.href))}return o}function f(e){e=u(e);var t=i.exec(e);return{protocol:t[1]?t[1].toLowerCase():"",slashes:!!t[2],rest:t[3]}}function h(e,t,n){if(e=u(e),!(this instanceof h))return new h(e,t,n);var a,i,s,l,d,v,m=c.slice(),g=typeof t,y=this,b=0;for("object"!==g&&"string"!==g&&(n=t,t=null),n&&"function"!=typeof n&&(n=o.parse),t=p(t),a=!(i=f(e||"")).protocol&&!i.slashes,y.slashes=i.slashes||a&&t.slashes,y.protocol=i.protocol||t.protocol||"",e=i.rest,i.slashes||(m[3]=[/(.*)/,"pathname"]);b=n.length?{value:void 0,done:!0}:(e=r(n,o),t.index+=e.length,{value:e,done:!1})}))},function(e,t,n){"use strict";e.exports=function(e){if("function"!=typeof e)throw new TypeError(e+" is not a function");return e}},function(e,t,n){e.exports=n(626)},function(e,t,n){e.exports=n(828)},function(e,t,n){"use strict";var r=n(870);e.exports=r},function(e,t,n){"use strict";n.r(t),n.d(t,"UPDATE_LAYOUT",(function(){return o})),n.d(t,"UPDATE_FILTER",(function(){return a})),n.d(t,"UPDATE_MODE",(function(){return i})),n.d(t,"SHOW",(function(){return s})),n.d(t,"updateLayout",(function(){return u})),n.d(t,"updateFilter",(function(){return c})),n.d(t,"show",(function(){return l})),n.d(t,"changeMode",(function(){return p}));var r=n(4),o="layout_update_layout",a="layout_update_filter",i="layout_update_mode",s="layout_show";function u(e){return{type:o,payload:e}}function c(e){return{type:a,payload:e}}function l(e){var t=!(arguments.length>1&&void 0!==arguments[1])||arguments[1];return e=Object(r.w)(e),{type:s,payload:{thing:e,shown:t}}}function p(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";return e=Object(r.w)(e),{type:i,payload:{thing:e,mode:t}}}},function(e,t,n){"use strict";var r=n(1052),o=n(1053);function a(){this.protocol=null,this.slashes=null,this.auth=null,this.host=null,this.port=null,this.hostname=null,this.hash=null,this.search=null,this.query=null,this.pathname=null,this.path=null,this.href=null}t.parse=b,t.resolve=function(e,t){return b(e,!1,!0).resolve(t)},t.resolveObject=function(e,t){return e?b(e,!1,!0).resolveObject(t):t},t.format=function(e){o.isString(e)&&(e=b(e));return e instanceof a?e.format():a.prototype.format.call(e)},t.Url=a;var i=/^([a-z0-9.+-]+:)/i,s=/:[0-9]*$/,u=/^(\/\/?(?!\/)[^\?\s]*)(\?[^\s]*)?$/,c=["{","}","|","\\","^","`"].concat(["<",">",'"',"`"," ","\r","\n","\t"]),l=["'"].concat(c),p=["%","/","?",";","#"].concat(l),f=["/","?","#"],h=/^[+a-z0-9A-Z_-]{0,63}$/,d=/^([+a-z0-9A-Z_-]{0,63})(.*)$/,v={javascript:!0,"javascript:":!0},m={javascript:!0,"javascript:":!0},g={http:!0,https:!0,ftp:!0,gopher:!0,file:!0,"http:":!0,"https:":!0,"ftp:":!0,"gopher:":!0,"file:":!0},y=n(1054);function b(e,t,n){if(e&&o.isObject(e)&&e instanceof a)return e;var r=new a;return r.parse(e,t,n),r}a.prototype.parse=function(e,t,n){if(!o.isString(e))throw new TypeError("Parameter 'url' must be a string, not "+typeof e);var a=e.indexOf("?"),s=-1!==a&&a127?M+="x":M+=N[P];if(!M.match(h)){var D=T.slice(0,k),L=T.slice(k+1),B=N.match(d);B&&(D.push(B[1]),L.unshift(B[2])),L.length&&(b="/"+L.join(".")+b),this.hostname=D.join(".");break}}}this.hostname.length>255?this.hostname="":this.hostname=this.hostname.toLowerCase(),j||(this.hostname=r.toASCII(this.hostname));var F=this.port?":"+this.port:"",U=this.hostname||"";this.host=U+F,this.href+=this.host,j&&(this.hostname=this.hostname.substr(1,this.hostname.length-2),"/"!==b[0]&&(b="/"+b))}if(!v[x])for(k=0,I=l.length;k0)&&n.host.split("@"))&&(n.auth=j.shift(),n.host=n.hostname=j.shift());return n.search=e.search,n.query=e.query,o.isNull(n.pathname)&&o.isNull(n.search)||(n.path=(n.pathname?n.pathname:"")+(n.search?n.search:"")),n.href=n.format(),n}if(!E.length)return n.pathname=null,n.search?n.path="/"+n.search:n.path=null,n.href=n.format(),n;for(var S=E.slice(-1)[0],A=(n.host||e.host||E.length>1)&&("."===S||".."===S)||""===S,k=0,O=E.length;O>=0;O--)"."===(S=E[O])?E.splice(O,1):".."===S?(E.splice(O,1),k++):k&&(E.splice(O,1),k--);if(!w&&!x)for(;k--;k)E.unshift("..");!w||""===E[0]||E[0]&&"/"===E[0].charAt(0)||E.unshift(""),A&&"/"!==E.join("/").substr(-1)&&E.push("");var j,T=""===E[0]||E[0]&&"/"===E[0].charAt(0);C&&(n.hostname=n.host=T?"":E.length?E.shift():"",(j=!!(n.host&&n.host.indexOf("@")>0)&&n.host.split("@"))&&(n.auth=j.shift(),n.host=n.hostname=j.shift()));return(w=w||n.host&&E.length)&&!T&&E.unshift(""),E.length?n.pathname=E.join("/"):(n.pathname=null,n.path=null),o.isNull(n.pathname)&&o.isNull(n.search)||(n.path=(n.pathname?n.pathname:"")+(n.search?n.search:"")),n.auth=e.auth||n.auth,n.slashes=n.slashes||e.slashes,n.href=n.format(),n},a.prototype.parseHost=function(){var e=this.host,t=s.exec(e);t&&(":"!==(t=t[0])&&(this.port=t.substr(1)),e=e.substr(0,e.length-t.length)),e&&(this.hostname=e)}},function(e,t,n){var r=n(48),o=n(173),a=n(106),i=n(69),s=n(175),u=n(52),c=n(353),l=Object.getOwnPropertyDescriptor;t.f=r?l:function(e,t){if(e=i(e),t=s(t,!0),c)try{return l(e,t)}catch(e){}if(u(e,t))return a(!o.f.call(e,t),e[t])}},function(e,t){e.exports=function(e,t){return{enumerable:!(1&e),configurable:!(2&e),writable:!(4&e),value:t}}},function(e,t){var n={}.toString;e.exports=function(e){return n.call(e).slice(8,-1)}},function(e,t,n){var r=n(79);e.exports=function(e,t,n){if(r(e),void 0===t)return e;switch(n){case 0:return function(){return e.call(t)};case 1:return function(n){return e.call(t,n)};case 2:return function(n,r){return e.call(t,n,r)};case 3:return function(n,r,o){return e.call(t,n,r,o)}}return function(){return e.apply(t,arguments)}}},function(e,t,n){var r,o=n(55),a=n(231),i=n(226),s=n(149),u=n(366),c=n(223),l=n(177),p=l("IE_PROTO"),f=function(){},h=function(e){return"