From 5a220caf39de27e5b6f5d80d864d912c7903b8b6 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Tue, 8 Feb 2022 08:04:51 +0100 Subject: [PATCH 01/67] docs: Add payload properties for user-admin post payload Adds an explanation of what the various properties in the payload object to the `user-admin` endpoint does. Most seem fairly self-explanatory, but I'm not entirely sure what `sendEmail` does. Requires validation by a third party before merging. --- website/docs/api/admin/user-admin.md | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/website/docs/api/admin/user-admin.md b/website/docs/api/admin/user-admin.md index 210a503db1..067c39f075 100644 --- a/website/docs/api/admin/user-admin.md +++ b/website/docs/api/admin/user-admin.md @@ -115,7 +115,17 @@ You can also search for users via the search API. It will preform a simple searc `POST https://unleash.host.com/api/admin/user-admin` -Creates a new use with the given root role. +Creates a new user with the given root role. + +**Payload properties** + +| Property name | Required | Description | Example value(s) | +|---------------|----------|---------------------------------------------------------------------------------|------------------------| +| `email` | Yes | The user's email address. | `"user@getunleash.io"` | +| `name` | Yes | The user's name | `"Some Name"` | +| `rootRole` | Yes | The role to assign to the user. Can be either the role's ID or its unique name. | `2`, `"Editor"` | +| `sendEmail` | No | Whether to send a registration email to the user or not. Defaults to `true`. | `false` | + **Body** @@ -128,12 +138,6 @@ Creates a new use with the given root role. } ``` -**Notes** - -- `email` - Required field. -- `rootRole` - can either be the role id or the unique name of the role (e.g: `Editor`). -- `sendEmail` - set to `true` if you want Unleash to send Welcome email to the new user. Do require the Unleash instance to be configured with email settings. - #### Return values: {#return-values} `201: Created` From 3610b284065cbf0d84f24dc0526f6bcecfda3714 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sat, 19 Feb 2022 11:03:35 +0000 Subject: [PATCH 02/67] chore(deps): update dependency eslint-config-prettier to v8.4.0 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 8ce8f129d4..82f95f0e5a 100644 --- a/package.json +++ b/package.json @@ -141,7 +141,7 @@ "eslint": "8.9.0", "eslint-config-airbnb-base": "15.0.0", "eslint-config-airbnb-typescript": "16.0.0", - "eslint-config-prettier": "8.3.0", + "eslint-config-prettier": "8.4.0", "eslint-plugin-import": "2.25.4", "eslint-plugin-prettier": "4.0.0", "faker": "5.5.3", diff --git a/yarn.lock b/yarn.lock index b236c29b2c..c639354d0a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2736,10 +2736,10 @@ eslint-config-airbnb-typescript@16.0.0: dependencies: eslint-config-airbnb-base "^15.0.0" -eslint-config-prettier@8.3.0: - version "8.3.0" - resolved "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.3.0.tgz" - integrity sha512-BgZuLUSeKzvlL/VUjx/Yb787VQ26RU3gGjA3iiFvdsp/2bMfVIWUVP7tjxtjS0e+HP409cPlPvNkQloz8C91ew== +eslint-config-prettier@8.4.0: + version "8.4.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.4.0.tgz#8e6d17c7436649e98c4c2189868562921ef563de" + integrity sha512-CFotdUcMY18nGRo5KGsnNxpznzhkopOcOo0InID+sgQssPrzjvsyKZPvOgymTFeHrFuC3Tzdf2YndhXtULK9Iw== eslint-import-resolver-node@^0.3.6: version "0.3.6" From 9f6b414e0959855b33c8fb107ab4b0f5d52e7ebd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20Conradi=20=C3=98sthus?= Date: Mon, 21 Feb 2022 10:45:05 +0100 Subject: [PATCH 03/67] docs: correct unleash proxy sdk docs --- website/docs/sdks/proxy-javascript.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/sdks/proxy-javascript.md b/website/docs/sdks/proxy-javascript.md index 288cbdf6e1..8d9c123270 100644 --- a/website/docs/sdks/proxy-javascript.md +++ b/website/docs/sdks/proxy-javascript.md @@ -21,7 +21,7 @@ npm install unleash-proxy-client **Step 2: Initialize the SDK** -You need to have an Unleash-hosted instance, and the proxy needs to be enabled. In addition you will need a proxy-specific `clientKey` in order to connect to the Unleash-hosted Proxy. For more on how to set up client keys, [consult the Unleash Proxy docs](unleash-proxy.md#configuration-variables). +You need to have an Unleash Proxy server running. In addition you will need a proxy-specific `clientKey` in order to connect to the Unleash Proxy. For more on how to set up client keys, [consult the Unleash Proxy docs](unleash-proxy.md#configuration-variables). ```js import { UnleashClient } from 'unleash-proxy-client'; From 34e5034547d4afc497dfcc12c7d50797631bee21 Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Mon, 21 Feb 2022 12:46:28 +0100 Subject: [PATCH 04/67] fix: reduce project overview query count to 2. (#1356) * fix: reduce project overview query count to 2. Previously we've been doing N+1 queries for projects, this now changes to doing one query for projects with feature counts, and then one query for membercounts for all projects and merging that with the first query. --- package.json | 2 +- src/lib/db/index.ts | 2 +- src/lib/db/project-store.ts | 68 +++++++++++++++++-- src/lib/services/project-service.ts | 19 +----- src/lib/types/stores/project-store.ts | 3 +- .../api/admin/project/features.e2e.test.ts | 2 +- src/test/e2e/api/client/feature.e2e.test.ts | 2 +- src/test/fixtures/fake-project-store.ts | 8 ++- yarn.lock | 20 +++++- 9 files changed, 96 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index 82f95f0e5a..67033a5650 100644 --- a/package.json +++ b/package.json @@ -104,7 +104,7 @@ "nodemailer": "^6.5.0", "owasp-password-strength-test": "^1.3.0", "parse-database-url": "^0.3.0", - "pg": "^8.7.1", + "pg": "^8.7.3", "pg-connection-string": "^2.5.0", "pkginfo": "^0.4.1", "prom-client": "^14.0.0", diff --git a/src/lib/db/index.ts b/src/lib/db/index.ts index 04dab0866f..8d67f87a93 100644 --- a/src/lib/db/index.ts +++ b/src/lib/db/index.ts @@ -51,7 +51,7 @@ export const createStores = ( contextFieldStore: new ContextFieldStore(db, getLogger), settingStore: new SettingStore(db, getLogger), userStore: new UserStore(db, getLogger), - projectStore: new ProjectStore(db, getLogger), + projectStore: new ProjectStore(db, eventBus, getLogger), tagStore: new TagStore(db, eventBus, getLogger), tagTypeStore: new TagTypeStore(db, eventBus, getLogger), addonStore: new AddonStore(db, eventBus, getLogger), diff --git a/src/lib/db/project-store.ts b/src/lib/db/project-store.ts index 44b622a156..ee351ee8a6 100644 --- a/src/lib/db/project-store.ts +++ b/src/lib/db/project-store.ts @@ -2,7 +2,7 @@ import { Knex } from 'knex'; import { Logger, LogProvider } from '../logger'; import NotFoundError from '../error/notfound-error'; -import { IProject } from '../types/model'; +import { IProject, IProjectWithCount } from '../types/model'; import { IProjectHealthUpdate, IProjectInsert, @@ -10,6 +10,9 @@ import { IProjectStore, } from '../types/stores/project-store'; import { DEFAULT_ENV } from '../util/constants'; +import metricsHelper from '../util/metrics-helper'; +import { DB_TIME } from '../metric-events'; +import EventEmitter from 'events'; const COLUMNS = [ 'id', @@ -26,9 +29,16 @@ class ProjectStore implements IProjectStore { private logger: Logger; - constructor(db: Knex, getLogger: LogProvider) { + private timer: Function; + + constructor(db: Knex, eventBus: EventEmitter, getLogger: LogProvider) { this.db = db; this.logger = getLogger('project-store.ts'); + this.timer = (action) => + metricsHelper.wrapTimer(eventBus, DB_TIME, { + store: 'project', + action, + }); } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types @@ -44,13 +54,61 @@ class ProjectStore implements IProjectStore { async exists(id: string): Promise { const result = await this.db.raw( - `SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE id = ?) AS present`, + `SELECT EXISTS(SELECT 1 FROM ${TABLE} WHERE id = ?) AS present`, [id], ); const { present } = result.rows[0]; return present; } + async getProjectsWithCounts( + query?: IProjectQuery, + ): Promise { + const projectTimer = this.timer('getProjectsWithCount'); + let projects = this.db(TABLE) + .select( + this.db.raw( + 'projects.id, projects.name, projects.description, projects.health, projects.updated_at, count(features.name) AS number_of_features', + ), + ) + .leftJoin('features', 'features.project', 'projects.id') + .groupBy('projects.id'); + if (query) { + projects = projects.where(query); + } + const projectAndFeatureCount = await projects; + + // @ts-ignore + const projectsWithFeatureCount = projectAndFeatureCount.map( + this.mapProjectWithCountRow, + ); + projectTimer(); + const memberTimer = this.timer('getMemberCount'); + const memberCount = await this.db.raw( + `SELECT count(role_id) as member_count, project FROM role_user GROUP BY project`, + ); + memberTimer(); + const memberMap = new Map( + memberCount.rows.map((c) => [c.project, Number(c.member_count)]), + ); + return projectsWithFeatureCount.map((r) => { + return { ...r, memberCount: memberMap.get(r.id) }; + }); + } + + // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types + mapProjectWithCountRow(row): IProjectWithCount { + return { + name: row.name, + id: row.id, + description: row.description, + health: row.health, + featureCount: row.number_of_features, + memberCount: row.number_of_users || 0, + updatedAt: row.updated_at, + }; + } + async getAll(query: IProjectQuery = {}): Promise { const rows = await this.db .select(COLUMNS) @@ -71,7 +129,7 @@ class ProjectStore implements IProjectStore { async hasProject(id: string): Promise { const result = await this.db.raw( - `SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE id = ?) AS present`, + `SELECT EXISTS(SELECT 1 FROM ${TABLE} WHERE id = ?) AS present`, [id], ); const { present } = result.rows[0]; @@ -208,5 +266,5 @@ class ProjectStore implements IProjectStore { }; } } + export default ProjectStore; -module.exports = ProjectStore; diff --git a/src/lib/services/project-service.ts b/src/lib/services/project-service.ts index 78aa7ccffe..01605f1e61 100644 --- a/src/lib/services/project-service.ts +++ b/src/lib/services/project-service.ts @@ -100,24 +100,7 @@ export default class ProjectService { } async getProjects(query?: IProjectQuery): Promise { - const projects = await this.store.getAll(query); - const projectsWithCount = await Promise.all( - projects.map(async (p) => { - let featureCount = 0; - let memberCount = 0; - try { - featureCount = - await this.featureToggleService.getFeatureCountForProject( - p.id, - ); - memberCount = await this.getMembers(p.id); - } catch (e) { - this.logger.warn('Error fetching project counts', e); - } - return { ...p, featureCount, memberCount }; - }), - ); - return projectsWithCount; + return this.store.getProjectsWithCounts(query); } async getProject(id: string): Promise { diff --git a/src/lib/types/stores/project-store.ts b/src/lib/types/stores/project-store.ts index 9d0448e066..2e4c41d8b5 100644 --- a/src/lib/types/stores/project-store.ts +++ b/src/lib/types/stores/project-store.ts @@ -1,4 +1,4 @@ -import { IProject } from '../model'; +import { IProject, IProjectWithCount } from '../model'; import { Store } from './store'; export interface IProjectInsert { @@ -32,6 +32,7 @@ export interface IProjectStore extends Store { deleteEnvironmentForProject(id: string, environment: string): Promise; getEnvironmentsForProject(id: string): Promise; getMembers(projectId: string): Promise; + getProjectsWithCounts(query?: IProjectQuery): Promise; count(): Promise; getAll(query?: IProjectQuery): Promise; } diff --git a/src/test/e2e/api/admin/project/features.e2e.test.ts b/src/test/e2e/api/admin/project/features.e2e.test.ts index d350182bb6..ab7bb3fd70 100644 --- a/src/test/e2e/api/admin/project/features.e2e.test.ts +++ b/src/test/e2e/api/admin/project/features.e2e.test.ts @@ -2039,7 +2039,7 @@ test('Can update impression data with PUT', async () => { }); test('Can create toggle with impression data on different project', async () => { - db.stores.projectStore.create({ + await db.stores.projectStore.create({ id: 'impression-data', name: 'ImpressionData', description: '', diff --git a/src/test/e2e/api/client/feature.e2e.test.ts b/src/test/e2e/api/client/feature.e2e.test.ts index a37178f54f..69dd13290d 100644 --- a/src/test/e2e/api/client/feature.e2e.test.ts +++ b/src/test/e2e/api/client/feature.e2e.test.ts @@ -282,7 +282,7 @@ test('returns a feature toggles impression data for a different project', async description: '', }; - db.stores.projectStore.create(project); + await db.stores.projectStore.create(project); const toggle = { name: 'project-client.impression.data', diff --git a/src/test/fixtures/fake-project-store.ts b/src/test/fixtures/fake-project-store.ts index 5a08f275a2..2494b508e2 100644 --- a/src/test/fixtures/fake-project-store.ts +++ b/src/test/fixtures/fake-project-store.ts @@ -3,7 +3,7 @@ import { IProjectInsert, IProjectStore, } from '../../lib/types/stores/project-store'; -import { IProject } from '../../lib/types/model'; +import { IProject, IProjectWithCount } from '../../lib/types/model'; import NotFoundError from '../../lib/error/notfound-error'; export default class FakeProjectStore implements IProjectStore { @@ -26,6 +26,12 @@ export default class FakeProjectStore implements IProjectStore { this.projectEnvironment.set(id, environments); } + async getProjectsWithCounts(): Promise { + return this.projects.map((p) => { + return { ...p, memberCount: 0, featureCount: 0 }; + }); + } + private createInternal(project: IProjectInsert): IProject { const newProj: IProject = { ...project, diff --git a/yarn.lock b/yarn.lock index c639354d0a..b4fa629cbf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5756,6 +5756,11 @@ pg-pool@^3.4.1: resolved "https://registry.npmjs.org/pg-pool/-/pg-pool-3.4.1.tgz" integrity sha512-TVHxR/gf3MeJRvchgNHxsYsTCHQ+4wm3VIHSS19z8NC0+gioEhq1okDY1sm/TYbfoP6JLFx01s0ShvZ3puP/iQ== +pg-pool@^3.5.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/pg-pool/-/pg-pool-3.5.1.tgz#f499ce76f9bf5097488b3b83b19861f28e4ed905" + integrity sha512-6iCR0wVrro6OOHFsyavV+i6KYL4lVNyYAB9RD18w66xSzN+d8b66HiwuP30Gp1SH5O9T82fckkzsRjlrhD0ioQ== + pg-protocol@^1.5.0: version "1.5.0" resolved "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.5.0.tgz" @@ -5772,7 +5777,7 @@ pg-types@^2.1.0: postgres-date "~1.0.4" postgres-interval "^1.1.0" -pg@^8.0.3, pg@^8.7.1: +pg@^8.0.3: version "8.7.1" resolved "https://registry.npmjs.org/pg/-/pg-8.7.1.tgz" integrity sha512-7bdYcv7V6U3KAtWjpQJJBww0UEsWuh4yQ/EjNf2HeO/NnvKjpvhEIe/A/TleP6wtmSKnUnghs5A9jUoK6iDdkA== @@ -5785,6 +5790,19 @@ pg@^8.0.3, pg@^8.7.1: pg-types "^2.1.0" pgpass "1.x" +pg@^8.7.3: + version "8.7.3" + resolved "https://registry.yarnpkg.com/pg/-/pg-8.7.3.tgz#8a5bdd664ca4fda4db7997ec634c6e5455b27c44" + integrity sha512-HPmH4GH4H3AOprDJOazoIcpI49XFsHCe8xlrjHkWiapdbHK+HLtbm/GQzXYAZwmPju/kzKhjaSfMACG+8cgJcw== + dependencies: + buffer-writer "2.0.0" + packet-reader "1.0.0" + pg-connection-string "^2.5.0" + pg-pool "^3.5.1" + pg-protocol "^1.5.0" + pg-types "^2.1.0" + pgpass "1.x" + pgpass@1.x: version "1.0.4" resolved "https://registry.npmjs.org/pgpass/-/pgpass-1.0.4.tgz" From 5487fca3183692a86da9e9df1326c3cdc9a52eb0 Mon Sep 17 00:00:00 2001 From: sighphyre Date: Mon, 21 Feb 2022 13:59:45 +0200 Subject: [PATCH 05/67] docs: add in compatibility table entry for advanced constraints --- website/docs/sdks/index.md | 1 + 1 file changed, 1 insertion(+) diff --git a/website/docs/sdks/index.md b/website/docs/sdks/index.md index 14ba30fd97..64c35f471b 100644 --- a/website/docs/sdks/index.md +++ b/website/docs/sdks/index.md @@ -76,6 +76,7 @@ If you see an item marked with a ❌ that you would find useful, feel free to re | Basic support | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | | **Category: [Strategy constraints](../advanced/strategy_constraints)** | | | | | | | | | | | Basic support (`IN`, `NOT_IN` operators) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | +| Advanced support (Semver, date, numeric and extended string operators) | ✅ | ✅ | ⭕ | ⭕ | ⭕ | ⭕ | ⭕ | ⭕ | | | **Category: [Unleash Context](../user_guide/unleash_context)** | | | | | | | | | | | Static fields (`environment`, `appName`) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | | Defined fields | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | From 18309f73b2bfa5b9645b9537ba6a79c59af6c283 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 21 Feb 2022 16:32:40 +0000 Subject: [PATCH 06/67] fix(deps): update dependency unleash-frontend to v4.8.0-beta.7 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 67033a5650..5a920ae830 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,7 @@ "serve-favicon": "^2.5.0", "stoppable": "^1.1.0", "type-is": "^1.6.18", - "unleash-frontend": "4.8.0-beta.5", + "unleash-frontend": "4.8.0-beta.7", "uuid": "^8.3.2" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index b4fa629cbf..8222dde51d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7259,10 +7259,10 @@ universalify@^2.0.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== -unleash-frontend@4.8.0-beta.5: - version "4.8.0-beta.5" - resolved "https://registry.yarnpkg.com/unleash-frontend/-/unleash-frontend-4.8.0-beta.5.tgz#bc7a3384864d7ddfcb8dcf83d07cf986ea68f456" - integrity sha512-ja7WAj5zLyRKJzKwY5EMpXbyW8Azj0wDVGLQ7v3p4wVNVplXmR5nb4XfnF64nD6BC2m4VjEzLt6tzgB3Fdn8BA== +unleash-frontend@4.8.0-beta.7: + version "4.8.0-beta.7" + resolved "https://registry.yarnpkg.com/unleash-frontend/-/unleash-frontend-4.8.0-beta.7.tgz#afac03f95e5d47da02585e7cff1a6650b05d732b" + integrity sha512-r6AvHDrqKbCPne91dm1JM4rZNZWd0HwHgBUY3D4ZniVHdr6tLYDy0/Ge5ouDiq8Mu4LGaVBH6YuY+rY4YA07GQ== unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" From 2734440bdc66656e0c51c9d6b77782a9e05a4629 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 21 Feb 2022 18:30:27 +0000 Subject: [PATCH 07/67] chore(deps): update typescript-eslint monorepo to v5.12.1 --- package.json | 4 +-- yarn.lock | 94 ++++++++++++++++++++++++++-------------------------- 2 files changed, 49 insertions(+), 49 deletions(-) diff --git a/package.json b/package.json index 5a920ae830..4b55221010 100644 --- a/package.json +++ b/package.json @@ -133,8 +133,8 @@ "@types/supertest": "2.0.11", "@types/type-is": "1.6.3", "@types/uuid": "8.3.4", - "@typescript-eslint/eslint-plugin": "5.12.0", - "@typescript-eslint/parser": "5.12.0", + "@typescript-eslint/eslint-plugin": "5.12.1", + "@typescript-eslint/parser": "5.12.1", "copyfiles": "2.4.1", "coveralls": "3.1.1", "del-cli": "4.0.1", diff --git a/yarn.lock b/yarn.lock index 8222dde51d..fecaf9406c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1215,14 +1215,14 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@5.12.0": - version "5.12.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.12.0.tgz#bb46dd7ce7015c0928b98af1e602118e97df6c70" - integrity sha512-fwCMkDimwHVeIOKeBHiZhRUfJXU8n6xW1FL9diDxAyGAFvKcH4csy0v7twivOQdQdA0KC8TDr7GGRd3L4Lv0rQ== +"@typescript-eslint/eslint-plugin@5.12.1": + version "5.12.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.12.1.tgz#b2cd3e288f250ce8332d5035a2ff65aba3374ac4" + integrity sha512-M499lqa8rnNK7mUv74lSFFttuUsubIRdAbHcVaP93oFcKkEmHmLqy2n7jM9C8DVmFMYK61ExrZU6dLYhQZmUpw== dependencies: - "@typescript-eslint/scope-manager" "5.12.0" - "@typescript-eslint/type-utils" "5.12.0" - "@typescript-eslint/utils" "5.12.0" + "@typescript-eslint/scope-manager" "5.12.1" + "@typescript-eslint/type-utils" "5.12.1" + "@typescript-eslint/utils" "5.12.1" debug "^4.3.2" functional-red-black-tree "^1.0.1" ignore "^5.1.8" @@ -1230,69 +1230,69 @@ semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/parser@5.12.0": - version "5.12.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.12.0.tgz#0ca669861813df99ce54916f66f524c625ed2434" - integrity sha512-MfSwg9JMBojMUoGjUmX+D2stoQj1CBYTCP0qnnVtu9A+YQXVKNtLjasYh+jozOcrb/wau8TCfWOkQTiOAruBog== +"@typescript-eslint/parser@5.12.1": + version "5.12.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.12.1.tgz#b090289b553b8aa0899740d799d0f96e6f49771b" + integrity sha512-6LuVUbe7oSdHxUWoX/m40Ni8gsZMKCi31rlawBHt7VtW15iHzjbpj2WLiToG2758KjtCCiLRKZqfrOdl3cNKuw== dependencies: - "@typescript-eslint/scope-manager" "5.12.0" - "@typescript-eslint/types" "5.12.0" - "@typescript-eslint/typescript-estree" "5.12.0" + "@typescript-eslint/scope-manager" "5.12.1" + "@typescript-eslint/types" "5.12.1" + "@typescript-eslint/typescript-estree" "5.12.1" debug "^4.3.2" -"@typescript-eslint/scope-manager@5.12.0": - version "5.12.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.12.0.tgz#59619e6e5e2b1ce6cb3948b56014d3a24da83f5e" - integrity sha512-GAMobtIJI8FGf1sLlUWNUm2IOkIjvn7laFWyRx7CLrv6nLBI7su+B7lbStqVlK5NdLvHRFiJo2HhiDF7Ki01WQ== +"@typescript-eslint/scope-manager@5.12.1": + version "5.12.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.12.1.tgz#58734fd45d2d1dec49641aacc075fba5f0968817" + integrity sha512-J0Wrh5xS6XNkd4TkOosxdpObzlYfXjAFIm9QxYLCPOcHVv1FyyFCPom66uIh8uBr0sZCrtS+n19tzufhwab8ZQ== dependencies: - "@typescript-eslint/types" "5.12.0" - "@typescript-eslint/visitor-keys" "5.12.0" + "@typescript-eslint/types" "5.12.1" + "@typescript-eslint/visitor-keys" "5.12.1" -"@typescript-eslint/type-utils@5.12.0": - version "5.12.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.12.0.tgz#aaf45765de71c6d9707c66ccff76ec2b9aa31bb6" - integrity sha512-9j9rli3zEBV+ae7rlbBOotJcI6zfc6SHFMdKI9M3Nc0sy458LJ79Os+TPWeBBL96J9/e36rdJOfCuyRSgFAA0Q== +"@typescript-eslint/type-utils@5.12.1": + version "5.12.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.12.1.tgz#8d58c6a0bb176b5e9a91581cda1a7f91a114d3f0" + integrity sha512-Gh8feEhsNLeCz6aYqynh61Vsdy+tiNNkQtc+bN3IvQvRqHkXGUhYkUi+ePKzP0Mb42se7FDb+y2SypTbpbR/Sg== dependencies: - "@typescript-eslint/utils" "5.12.0" + "@typescript-eslint/utils" "5.12.1" debug "^4.3.2" tsutils "^3.21.0" -"@typescript-eslint/types@5.12.0": - version "5.12.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.12.0.tgz#5b4030a28222ee01e851836562c07769eecda0b8" - integrity sha512-JowqbwPf93nvf8fZn5XrPGFBdIK8+yx5UEGs2QFAYFI8IWYfrzz+6zqlurGr2ctShMaJxqwsqmra3WXWjH1nRQ== +"@typescript-eslint/types@5.12.1": + version "5.12.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.12.1.tgz#46a36a28ff4d946821b58fe5a73c81dc2e12aa89" + integrity sha512-hfcbq4qVOHV1YRdhkDldhV9NpmmAu2vp6wuFODL71Y0Ixak+FLeEU4rnPxgmZMnGreGEghlEucs9UZn5KOfHJA== -"@typescript-eslint/typescript-estree@5.12.0": - version "5.12.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.12.0.tgz#cabf545fd592722f0e2b4104711e63bf89525cd2" - integrity sha512-Dd9gVeOqt38QHR0BEA8oRaT65WYqPYbIc5tRFQPkfLquVEFPD1HAtbZT98TLBkEcCkvwDYOAvuSvAD9DnQhMfQ== +"@typescript-eslint/typescript-estree@5.12.1": + version "5.12.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.12.1.tgz#6a9425b9c305bcbc38e2d1d9a24c08e15e02b722" + integrity sha512-ahOdkIY9Mgbza7L9sIi205Pe1inCkZWAHE1TV1bpxlU4RZNPtXaDZfiiFWcL9jdxvW1hDYZJXrFm+vlMkXRbBw== dependencies: - "@typescript-eslint/types" "5.12.0" - "@typescript-eslint/visitor-keys" "5.12.0" + "@typescript-eslint/types" "5.12.1" + "@typescript-eslint/visitor-keys" "5.12.1" debug "^4.3.2" globby "^11.0.4" is-glob "^4.0.3" semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/utils@5.12.0": - version "5.12.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.12.0.tgz#92fd3193191621ab863add2f553a7b38b65646af" - integrity sha512-k4J2WovnMPGI4PzKgDtQdNrCnmBHpMUFy21qjX2CoPdoBcSBIMvVBr9P2YDP8jOqZOeK3ThOL6VO/sy6jtnvzw== +"@typescript-eslint/utils@5.12.1": + version "5.12.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.12.1.tgz#447c24a05d9c33f9c6c64cb48f251f2371eef920" + integrity sha512-Qq9FIuU0EVEsi8fS6pG+uurbhNTtoYr4fq8tKjBupsK5Bgbk2I32UGm0Sh+WOyjOPgo/5URbxxSNV6HYsxV4MQ== dependencies: "@types/json-schema" "^7.0.9" - "@typescript-eslint/scope-manager" "5.12.0" - "@typescript-eslint/types" "5.12.0" - "@typescript-eslint/typescript-estree" "5.12.0" + "@typescript-eslint/scope-manager" "5.12.1" + "@typescript-eslint/types" "5.12.1" + "@typescript-eslint/typescript-estree" "5.12.1" eslint-scope "^5.1.1" eslint-utils "^3.0.0" -"@typescript-eslint/visitor-keys@5.12.0": - version "5.12.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.12.0.tgz#1ac9352ed140b07ba144ebf371b743fdf537ec16" - integrity sha512-cFwTlgnMV6TgezQynx2c/4/tx9Tufbuo9LPzmWqyRC3QC4qTGkAG1C6pBr0/4I10PAI/FlYunI3vJjIcu+ZHMg== +"@typescript-eslint/visitor-keys@5.12.1": + version "5.12.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.12.1.tgz#f722da106c8f9695ae5640574225e45af3e52ec3" + integrity sha512-l1KSLfupuwrXx6wc0AuOmC7Ko5g14ZOQ86wJJqRbdLbXLK02pK/DPiDDqCc7BqqiiA04/eAA6ayL0bgOrAkH7A== dependencies: - "@typescript-eslint/types" "5.12.0" + "@typescript-eslint/types" "5.12.1" eslint-visitor-keys "^3.0.0" abab@^2.0.3, abab@^2.0.5: From e62cbb6edac9f6ceab3c0a9ad9dde50473c76523 Mon Sep 17 00:00:00 2001 From: Fredrik Oseberg Date: Tue, 22 Feb 2022 22:59:26 +0100 Subject: [PATCH 08/67] chore: update fronted to version 4.8.0-beta.8 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 4b55221010..030e74bed2 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,7 @@ "serve-favicon": "^2.5.0", "stoppable": "^1.1.0", "type-is": "^1.6.18", - "unleash-frontend": "4.8.0-beta.7", + "unleash-frontend": "4.8.0-beta.8", "uuid": "^8.3.2" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index fecaf9406c..70e7a96a0e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7259,10 +7259,10 @@ universalify@^2.0.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== -unleash-frontend@4.8.0-beta.7: - version "4.8.0-beta.7" - resolved "https://registry.yarnpkg.com/unleash-frontend/-/unleash-frontend-4.8.0-beta.7.tgz#afac03f95e5d47da02585e7cff1a6650b05d732b" - integrity sha512-r6AvHDrqKbCPne91dm1JM4rZNZWd0HwHgBUY3D4ZniVHdr6tLYDy0/Ge5ouDiq8Mu4LGaVBH6YuY+rY4YA07GQ== +unleash-frontend@4.8.0-beta.8: + version "4.8.0-beta.8" + resolved "https://registry.yarnpkg.com/unleash-frontend/-/unleash-frontend-4.8.0-beta.8.tgz#dc6fda6382c2241b667300be5f99027212822a46" + integrity sha512-9Jlq1bowdw/i0pn5TDLy1fYx6k+HoSb5D7Xf5/lHk7B2el1Bu2e40yblOttXjC/jpoE8JSjvfhnccf7WSJ8pNg== unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" From e66466b2fb198fbc62927eba9de6a0022d0b9c6f Mon Sep 17 00:00:00 2001 From: Fredrik Oseberg Date: Tue, 22 Feb 2022 22:59:47 +0100 Subject: [PATCH 09/67] 4.8.0-beta.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 030e74bed2..aa581362aa 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "unleash-server", "description": "Unleash is an enterprise ready feature toggles service. It provides different strategies for handling feature toggles.", - "version": "4.8.0-beta.1", + "version": "4.8.0-beta.2", "keywords": [ "unleash", "feature toggle", From 435f64ad22aa3a76344c755b1a393b30542d29bc Mon Sep 17 00:00:00 2001 From: sighphyre Date: Wed, 23 Feb 2022 12:29:30 +0200 Subject: [PATCH 10/67] docs: mark PHP as advanced constraint compatible in compatibility table --- website/docs/sdks/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/sdks/index.md b/website/docs/sdks/index.md index 64c35f471b..dfe45954ce 100644 --- a/website/docs/sdks/index.md +++ b/website/docs/sdks/index.md @@ -76,7 +76,7 @@ If you see an item marked with a ❌ that you would find useful, feel free to re | Basic support | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | | **Category: [Strategy constraints](../advanced/strategy_constraints)** | | | | | | | | | | | Basic support (`IN`, `NOT_IN` operators) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | -| Advanced support (Semver, date, numeric and extended string operators) | ✅ | ✅ | ⭕ | ⭕ | ⭕ | ⭕ | ⭕ | ⭕ | | +| Advanced support (Semver, date, numeric and extended string operators) | ✅ | ✅ | ⭕ | ⭕ | ⭕ | ⭕ | ✅ | ⭕ | | | **Category: [Unleash Context](../user_guide/unleash_context)** | | | | | | | | | | | Static fields (`environment`, `appName`) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | | Defined fields | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | From 2580bb0df33736bb56ae86935fc4975773fd8f37 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 23 Feb 2022 09:11:57 +0000 Subject: [PATCH 11/67] chore(deps): update dependency @types/jest to v27.4.1 --- package.json | 2 +- yarn.lock | 34 +++++++--------------------------- 2 files changed, 8 insertions(+), 28 deletions(-) diff --git a/package.json b/package.json index aa581362aa..791d671328 100644 --- a/package.json +++ b/package.json @@ -121,7 +121,7 @@ "@types/express": "4.17.13", "@types/express-session": "1.17.4", "@types/faker": "5.5.9", - "@types/jest": "27.4.0", + "@types/jest": "27.4.1", "@types/js-yaml": "4.0.5", "@types/memoizee": "0.4.7", "@types/mime": "2.0.3", diff --git a/yarn.lock b/yarn.lock index 70e7a96a0e..6950d91281 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1068,12 +1068,12 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@27.4.0": - version "27.4.0" - resolved "https://registry.npmjs.org/@types/jest/-/jest-27.4.0.tgz" - integrity sha512-gHl8XuC1RZ8H2j5sHv/JqsaxXkDDM9iDOgu0Wp8sjs4u/snb2PVehyWXJPr+ORA0RPpgw231mnutWI1+0hgjIQ== +"@types/jest@27.4.1": + version "27.4.1" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-27.4.1.tgz#185cbe2926eaaf9662d340cc02e548ce9e11ab6d" + integrity sha512-23iPJADSmicDVrWk+HT58LMJtzLAnB2AgIzplQuq/bSrGaxCrlvRFjGbXmamnnk/mAmCdLStiGqggu28ocUyiw== dependencies: - jest-diff "^27.0.0" + jest-matcher-utils "^27.0.0" pretty-format "^27.0.0" "@types/js-yaml@4.0.5": @@ -2500,11 +2500,6 @@ dicer@0.2.5: readable-stream "1.1.x" streamsearch "0.1.2" -diff-sequences@^27.0.6: - version "27.0.6" - resolved "https://registry.npmjs.org/diff-sequences/-/diff-sequences-27.0.6.tgz" - integrity sha512-ag6wfpBFyNXZ0p8pcuIDS//D8H062ZQJ3fzYxjpmeKjnz8W4pekL3AI8VohmyZmsWW2PWaHgjsmqR6L13101VQ== - diff-sequences@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.5.1.tgz#eaecc0d327fd68c8d9672a1e64ab8dccb2ef5327" @@ -4273,16 +4268,6 @@ jest-config@^27.5.1: slash "^3.0.0" strip-json-comments "^3.1.1" -jest-diff@^27.0.0: - version "27.0.6" - resolved "https://registry.npmjs.org/jest-diff/-/jest-diff-27.0.6.tgz" - integrity sha512-Z1mqgkTCSYaFgwTlP/NUiRzdqgxmmhzHY1Tq17zL94morOHfHu3K4bgSgl+CR4GLhpV8VxkuOYuIWnQ9LnFqmg== - dependencies: - chalk "^4.0.0" - diff-sequences "^27.0.6" - jest-get-type "^27.0.6" - pretty-format "^27.0.6" - jest-diff@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-27.5.1.tgz#a07f5011ac9e6643cf8a95a462b7b1ecf6680def" @@ -4344,11 +4329,6 @@ jest-fetch-mock@3.0.3: cross-fetch "^3.0.4" promise-polyfill "^8.1.3" -jest-get-type@^27.0.6: - version "27.0.6" - resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-27.0.6.tgz" - integrity sha512-XTkK5exIeUbbveehcSR8w0bhH+c0yloW/Wpl+9vZrjzztCPWrxhHwkIFpZzCt71oRBsgxmuUfxEqOYoZI2macg== - jest-get-type@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-27.5.1.tgz#3cd613c507b0f7ace013df407a1c1cd578bcb4f1" @@ -4405,7 +4385,7 @@ jest-leak-detector@^27.5.1: jest-get-type "^27.5.1" pretty-format "^27.5.1" -jest-matcher-utils@^27.5.1: +jest-matcher-utils@^27.0.0, jest-matcher-utils@^27.5.1: version "27.5.1" resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-27.5.1.tgz#9c0cdbda8245bc22d2331729d1091308b40cf8ab" integrity sha512-z2uTx/T6LBaCoNWNFWwChLBKYxTMcGBRjAt+2SbP929/Fflb9aa5LGma654Rz8z9HLxsrUaYzxE9T/EFIL/PAw== @@ -5886,7 +5866,7 @@ prettier@2.5.1: resolved "https://registry.npmjs.org/prettier/-/prettier-2.5.1.tgz" integrity sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg== -pretty-format@^27.0.0, pretty-format@^27.0.6: +pretty-format@^27.0.0: version "27.0.6" resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-27.0.6.tgz" integrity sha512-8tGD7gBIENgzqA+UBzObyWqQ5B778VIFZA/S66cclyd5YkFLYs2Js7gxDKf0MXtTc9zcS7t1xhdfcElJ3YIvkQ== From a7fa3f703dc4c01586c825f486cd39a62eaca2ad Mon Sep 17 00:00:00 2001 From: sighphyre Date: Wed, 23 Feb 2022 15:30:13 +0200 Subject: [PATCH 12/67] docs: update docs for Go SDK because wait until initialized already exists --- website/docs/sdks/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/sdks/index.md b/website/docs/sdks/index.md index 14ba30fd97..63f0626995 100644 --- a/website/docs/sdks/index.md +++ b/website/docs/sdks/index.md @@ -53,7 +53,7 @@ If you see an item marked with a ❌ that you would find useful, feel free to re |---------------------------------------------------------------------------------------------------|:----------------------:|:-------------------------:|:------------------:|:--------------------------:|:----------------------:|:-------------------------:|:--------------------:|:------------------------------------------------------:|:----------------------------------------:| | **Category: Initialization** | | | | | | | | | | | Async initialization | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | N/A | -| Can block until synchronized | ✅ | ✅ | ⭕ | ⭕ | ⭕ | ✅ | ⭕ | ⭕ | N/A | +| Can block until synchronized | ✅ | ✅ | ✅ | ⭕ | ⭕ | ✅ | ⭕ | ⭕ | N/A | | Default refresh interval | 10s | 15s | 15s | 15s | 15s | 30s | 30s | 15s | 5s | | Default metrics interval | 60s | 60s | 60s | 60s | 60s | 60s | 30s | 15s | 30s | | Context provider | ✅ | N/A | N/A | N/A | N/A | ✅ | ✅ | N/A | N/A | From f09782208455269b83cc32a7d1454508a1920b17 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 23 Feb 2022 16:41:06 +0000 Subject: [PATCH 13/67] fix(deps): update dependency unleash-frontend to v4.8.0-beta.10 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 791d671328..873e65044c 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,7 @@ "serve-favicon": "^2.5.0", "stoppable": "^1.1.0", "type-is": "^1.6.18", - "unleash-frontend": "4.8.0-beta.8", + "unleash-frontend": "4.8.0-beta.10", "uuid": "^8.3.2" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index 6950d91281..c1258db857 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7239,10 +7239,10 @@ universalify@^2.0.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== -unleash-frontend@4.8.0-beta.8: - version "4.8.0-beta.8" - resolved "https://registry.yarnpkg.com/unleash-frontend/-/unleash-frontend-4.8.0-beta.8.tgz#dc6fda6382c2241b667300be5f99027212822a46" - integrity sha512-9Jlq1bowdw/i0pn5TDLy1fYx6k+HoSb5D7Xf5/lHk7B2el1Bu2e40yblOttXjC/jpoE8JSjvfhnccf7WSJ8pNg== +unleash-frontend@4.8.0-beta.10: + version "4.8.0-beta.10" + resolved "https://registry.yarnpkg.com/unleash-frontend/-/unleash-frontend-4.8.0-beta.10.tgz#601a8c8dafcf6baae0da42445e6538df5da8ec10" + integrity sha512-5CTPXZLnWtZAetUBZOYn5HnIlJkiH+/daqIxC2g8mfJhCja0Pu8wZXTm8NligtlzAl5z2JGHPi7a7UC3iz6ilw== unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" From bec32f726b82187f17f82d80e9915527551ce782 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20Conradi=20=C3=98sthus?= Date: Thu, 24 Feb 2022 08:24:47 +0100 Subject: [PATCH 14/67] fix: correct oas for creating feature toggle --- docs/api/oas/openapi.yaml | 94 +++++++++++++++++++++------------------ 1 file changed, 50 insertions(+), 44 deletions(-) diff --git a/docs/api/oas/openapi.yaml b/docs/api/oas/openapi.yaml index 9626cced61..5d9af6435e 100644 --- a/docs/api/oas/openapi.yaml +++ b/docs/api/oas/openapi.yaml @@ -39,13 +39,17 @@ tags: info: title: Unleash API description: |- + + > The Open API specifications are currently considered a **"beta feature"** and will not cover the full Unleash Admin API. + > You can follow the progress on making OAS official in [GitHub issue 1391](https://github.com/Unleash/unleash/issues/1391) + Unleash is an open source feature flag and toggle system for all your applications and services. # Try it out ## Try it in your browser - Once you have [set your Unleash server up](https://unleash.github.io/docs/getting_started), you can test the API from inside your browser. The following assumes the server is running on localhost:4242. + Once you have [set your Unleash server up](https://docs.getunleash.io/deploy/getting_started), you can test the API from inside your browser. The following assumes the server is running on localhost:4242. The following 'endpoints' (such as `GET /admin/metrics/applications`) provide reference documentation for the Unleash REST API. To try out API calls: 1. Navigate to an endpoint @@ -63,7 +67,7 @@ info: version: 4.0.13 contact: name: The Unleash team - url: 'https://unleash.github.io/' + url: 'https://docs.getunleash.io' externalDocs: description: Unleash documentation url: 'https://unleash.github.io/docs/getting_started' @@ -223,7 +227,7 @@ paths: source: | curl --request POST \ --url http://localhost:4242/api/admin/features \ - --data '[{"name":"featureX","description":"Toggles featureX on and off","type":"release","enabled":true,"stale":false,"strategies":[{"name":"default","editable":true,"description":"Default on/off strategy.","parameters":{"parameter":{"name":"groupId","type":"string","description":"Define activation groups to allow you to correlate across feature toggles.","required":false}}}],"variants":[{"name":"yellow","weight":20}],"createdAt":"string"}]' + --data '{"name":"featureX","description":"Toggles featureX on and off","type":"release","enabled":true,"stale":false,"strategies":[{"name":"default","editable":true,"description":"Default on/off strategy.","parameters":{"parameter":{"name":"groupId","type":"string","description":"Define activation groups to allow you to correlate across feature toggles.","required":false}}}],"variants":[{"name":"yellow","weight":20}],"createdAt":"string"}' '/admin/features/{featureName}': get: summary: Fetches a specific Feature Toggle from the Unleash server. @@ -917,7 +921,7 @@ components: version: $ref: '#/components/schemas/versionSchema' features: - $ref: '#/components/schemas/featureToggleSchema' + $ref: '#/components/schemas/featureToggleListSchema' x-tags: - Responses '401': @@ -1276,7 +1280,7 @@ components: minLength: 1 example: '2020-11-13T16:56:29.279Z' seenToggles: - $ref: '#/components/schemas/featureToggleSchema' + $ref: '#/components/schemas/featureToggleListSchema' links: type: object properties: @@ -1312,46 +1316,48 @@ components: example: 1 x-tags: - Schemas - featureToggleSchema: + featureToggleListSchema: type: array items: - type: object - required: - - name - - description - - type - - enabled - - stale - - strategies - properties: - name: - description: Feature Toggle name must be unique. - type: string - minLength: 1 - example: featureX - description: - type: string - minLength: 1 - example: Toggles featureX on and off - type: - $ref: '#/components/schemas/featureToggleTypeSchema' - enabled: - description: Is the Feature Toggle enabled? - type: boolean - example: true - stale: - description: Is the Feature Toggle 'stale' (deprecated)? - type: boolean - example: false - strategies: - $ref: '#/components/schemas/strategySchema' - variants: - $ref: '#/components/schemas/variantsSchema' - createdAt: - type: string - minLength: 1 - x-tags: - - Schemas + type: array + featureToggleSchema: + type: object + required: + - name + - description + - type + - enabled + - stale + - strategies + properties: + name: + description: Feature Toggle name must be unique. + type: string + minLength: 1 + example: featureX + description: + type: string + minLength: 1 + example: Toggles featureX on and off + type: + $ref: '#/components/schemas/featureToggleTypeSchema' + enabled: + description: Is the Feature Toggle enabled? + type: boolean + example: true + stale: + description: Is the Feature Toggle 'stale' (deprecated)? + type: boolean + example: false + strategies: + $ref: '#/components/schemas/strategySchema' + variants: + $ref: '#/components/schemas/variantsSchema' + createdAt: + type: string + minLength: 1 + x-tags: + - Schemas strategySchema: type: array items: @@ -1607,7 +1613,7 @@ components: version: $ref: '#/components/schemas/versionSchema' features: - $ref: '#/components/schemas/featureToggleSchema' + $ref: '#/components/schemas/featureToggleListSchema' strategies: $ref: '#/components/schemas/strategySchema' x-tags: From 6e09fa424e12242608448229648e9d58df8035e9 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Thu, 24 Feb 2022 09:39:49 +0000 Subject: [PATCH 15/67] fix(deps): update dependency unleash-frontend to v4.8.0 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 873e65044c..4ce045c77b 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,7 @@ "serve-favicon": "^2.5.0", "stoppable": "^1.1.0", "type-is": "^1.6.18", - "unleash-frontend": "4.8.0-beta.10", + "unleash-frontend": "4.8.0", "uuid": "^8.3.2" }, "devDependencies": { diff --git a/yarn.lock b/yarn.lock index c1258db857..cf7feb0e4e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7239,10 +7239,10 @@ universalify@^2.0.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== -unleash-frontend@4.8.0-beta.10: - version "4.8.0-beta.10" - resolved "https://registry.yarnpkg.com/unleash-frontend/-/unleash-frontend-4.8.0-beta.10.tgz#601a8c8dafcf6baae0da42445e6538df5da8ec10" - integrity sha512-5CTPXZLnWtZAetUBZOYn5HnIlJkiH+/daqIxC2g8mfJhCja0Pu8wZXTm8NligtlzAl5z2JGHPi7a7UC3iz6ilw== +unleash-frontend@4.8.0: + version "4.8.0" + resolved "https://registry.yarnpkg.com/unleash-frontend/-/unleash-frontend-4.8.0.tgz#c35961a740407fc0f4ab4ba24ea61f6bab02183b" + integrity sha512-GFT16muJ5ff9Xuz12OtZfsVC8ClTqEpoAP5tT0txNkUTauUSL1hTrvQsTeSJlY5THI7nKLrSBOGsKo0ysyiCXQ== unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" From 48e09dbc2a0aeae3f16e8a9efbba829dfb7a6f75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20Conradi=20=C3=98sthus?= Date: Thu, 24 Feb 2022 10:43:23 +0100 Subject: [PATCH 16/67] fix: add migration patch This patch will fix issues introduced in Unleash v3.13.0. It only affects users trying to upgrade from v3 to v4 and has been using v3.13.0 which contained an incorrect migration file. --- .../20210428103922-patch-role-table.js | 9 ++++ .../20210428103924-patch-admin-role-name.js | 14 +++++ .../20210428103924-patch-admin_role.js | 27 ++++++++++ .../20210428103924-patch-role_permissions.js | 53 +++++++++++++++++++ ...081422-remove-project-column-from-roles.js | 8 +++ 5 files changed, 111 insertions(+) create mode 100644 src/migrations/20210428103922-patch-role-table.js create mode 100644 src/migrations/20210428103924-patch-admin-role-name.js create mode 100644 src/migrations/20210428103924-patch-admin_role.js create mode 100644 src/migrations/20210428103924-patch-role_permissions.js create mode 100644 src/migrations/20220224081422-remove-project-column-from-roles.js diff --git a/src/migrations/20210428103922-patch-role-table.js b/src/migrations/20210428103922-patch-role-table.js new file mode 100644 index 0000000000..451b7fe8f8 --- /dev/null +++ b/src/migrations/20210428103922-patch-role-table.js @@ -0,0 +1,9 @@ +'use strict'; + +exports.up = function (db, cb) { + db.runSql('ALTER TABLE roles ADD COLUMN IF NOT EXISTS project text', cb); +}; + +exports.down = function (db, cb) { + cb(); +}; diff --git a/src/migrations/20210428103924-patch-admin-role-name.js b/src/migrations/20210428103924-patch-admin-role-name.js new file mode 100644 index 0000000000..8e4bf6eb05 --- /dev/null +++ b/src/migrations/20210428103924-patch-admin-role-name.js @@ -0,0 +1,14 @@ +'use strict'; + +exports.up = function (db, cb) { + db.runSql( + ` + UPDATE roles set name='Admin' where name='Super User'; + `, + cb, + ); +}; + +exports.down = function (db, cb) { + cb(); +}; diff --git a/src/migrations/20210428103924-patch-admin_role.js b/src/migrations/20210428103924-patch-admin_role.js new file mode 100644 index 0000000000..891e6f02ab --- /dev/null +++ b/src/migrations/20210428103924-patch-admin_role.js @@ -0,0 +1,27 @@ +'use strict'; + +exports.up = function (db, cb) { + db.runSql( + ` + DO $$ + declare + begin + WITH admin AS ( + SELECT * FROM roles WHERE name in ('Admin', 'Super User') LIMIT 1 + ) + INSERT into role_user(role_id, user_id) + VALUES + ((select id from admin), (select id FROM users where username='admin' LIMIT 1)); + + EXCEPTION WHEN OTHERS THEN + raise notice 'Ignored'; + end; + $$;`, + 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/20210428103924-patch-role_permissions.js b/src/migrations/20210428103924-patch-role_permissions.js new file mode 100644 index 0000000000..ecd74e719d --- /dev/null +++ b/src/migrations/20210428103924-patch-role_permissions.js @@ -0,0 +1,53 @@ +'use strict'; + +exports.up = function (db, cb) { + db.runSql( + ` + DO $$ + declare + begin + WITH editor AS ( + SELECT * FROM roles WHERE name in ('Editor', 'Regular') LIMIT 1 + ) + + INSERT INTO role_permission(role_id, project, permission) + VALUES + ((SELECT id from editor), '', 'CREATE_STRATEGY'), + ((SELECT id from editor), '', 'UPDATE_STRATEGY'), + ((SELECT id from editor), '', 'DELETE_STRATEGY'), + + ((SELECT id from editor), '', 'UPDATE_APPLICATION'), + + ((SELECT id from editor), '', 'CREATE_CONTEXT_FIELD'), + ((SELECT id from editor), '', 'UPDATE_CONTEXT_FIELD'), + ((SELECT id from editor), '', 'DELETE_CONTEXT_FIELD'), + + ((SELECT id from editor), '', 'CREATE_PROJECT'), + + ((SELECT id from editor), '', 'CREATE_ADDON'), + ((SELECT id from editor), '', 'UPDATE_ADDON'), + ((SELECT id from editor), '', 'DELETE_ADDON'), + + ((SELECT id from editor), 'default', 'UPDATE_PROJECT'), + ((SELECT id from editor), 'default', 'DELETE_PROJECT'), + ((SELECT id from editor), 'default', 'CREATE_FEATURE'), + ((SELECT id from editor), 'default', 'UPDATE_FEATURE'), + ((SELECT id from editor), 'default', 'DELETE_FEATURE'); + + -- Clean up duplicates + DELETE FROM role_permission p1 + USING role_permission p2 + WHERE p1.created_at < p2.created_at -- select the "older" ones + AND p1.project = p2.project -- list columns that define duplicates + AND p1.permission = p2.permission; + EXCEPTION WHEN OTHERS THEN + raise notice 'Ignored'; + end; + $$;`, + cb, + ); +}; + +exports.down = function (db, cb) { + cb(); +}; diff --git a/src/migrations/20220224081422-remove-project-column-from-roles.js b/src/migrations/20220224081422-remove-project-column-from-roles.js new file mode 100644 index 0000000000..840ea50de8 --- /dev/null +++ b/src/migrations/20220224081422-remove-project-column-from-roles.js @@ -0,0 +1,8 @@ +exports.up = function (db, cb) { + db.runSql('ALTER TABLE roles DROP COLUMN IF EXISTS project', 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. + db.runSql('ALTER TABLE roles ADD COLUMN IF NOT EXISTS project text', cb); +}; From 2d4727d25db35de5f3481de5bb890854e8c1dfa5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20Conradi=20=C3=98sthus?= Date: Thu, 24 Feb 2022 10:46:22 +0100 Subject: [PATCH 17/67] fix: remove project column from roles if exists --- .../20220224081422-remove-project-column-from-roles.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/migrations/20220224081422-remove-project-column-from-roles.js b/src/migrations/20220224081422-remove-project-column-from-roles.js index 840ea50de8..ffd166931a 100644 --- a/src/migrations/20220224081422-remove-project-column-from-roles.js +++ b/src/migrations/20220224081422-remove-project-column-from-roles.js @@ -3,6 +3,5 @@ exports.up = 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. db.runSql('ALTER TABLE roles ADD COLUMN IF NOT EXISTS project text', cb); }; From d401a8bd596e07d8387b8a2ef30350f5162e0b0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20Conradi=20=C3=98sthus?= Date: Thu, 24 Feb 2022 14:12:04 +0100 Subject: [PATCH 18/67] 4.8.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4ce045c77b..2c3fda030d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "unleash-server", "description": "Unleash is an enterprise ready feature toggles service. It provides different strategies for handling feature toggles.", - "version": "4.8.0-beta.2", + "version": "4.8.0", "keywords": [ "unleash", "feature toggle", From 3704f93ff56446d728f98f4d1718d591e1f9829f Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Fri, 25 Feb 2022 08:32:12 +0100 Subject: [PATCH 19/67] fix: readd orderBy statement to project query (#1394) --- src/lib/db/project-store.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/db/project-store.ts b/src/lib/db/project-store.ts index ee351ee8a6..2c08a845d5 100644 --- a/src/lib/db/project-store.ts +++ b/src/lib/db/project-store.ts @@ -72,7 +72,8 @@ class ProjectStore implements IProjectStore { ), ) .leftJoin('features', 'features.project', 'projects.id') - .groupBy('projects.id'); + .groupBy('projects.id') + .orderBy('projects.name', 'asc'); if (query) { projects = projects.where(query); } From 3302e66ab1a7ecd6b21192238f6332d5fa873959 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Fri, 25 Feb 2022 09:10:22 +0100 Subject: [PATCH 20/67] docs: remove "future enhancements" section of environments doc These things are either implemented or just around the corner now. I suggest removing them to not be misleading anymore. Case in point: we just had a customer asking us when the permissions system would be ready because the docs say it's planned as a future improvement. --- website/docs/user_guide/environments.md | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/website/docs/user_guide/environments.md b/website/docs/user_guide/environments.md index 029bcb8469..35b721272c 100644 --- a/website/docs/user_guide/environments.md +++ b/website/docs/user_guide/environments.md @@ -146,13 +146,3 @@ In order to support configuration per environment we had to rebuild our feature * **Unleash v4.2** will provide _early access_ to environment support. This means that it can be enabled per customer via a feature flag. * **Unleash v4.3** plans to provide general access to the environment support for all users of Unleash (Open-Source, Pro, Enterprise). - - -### Future enhancements - -With improved environment capabilities we have also done the groundwork to be able to also improve other related aspects inside Unleash: - - - -* Improve **Usage Metrics** to be able to show usage and evaluation results per hour for multiple days with dimensions such as environment, application and time (per hour). -* Improve **RBAC** with the ability to limit who can change configuration for a specific environment (planned as an enterprise feature). From 8906bf99fff7ea2b8d0cf9181b106a9086f29f88 Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Fri, 25 Feb 2022 11:30:15 +0100 Subject: [PATCH 21/67] 4.8.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2c3fda030d..e5a7246ca2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "unleash-server", "description": "Unleash is an enterprise ready feature toggles service. It provides different strategies for handling feature toggles.", - "version": "4.8.0", + "version": "4.8.1", "keywords": [ "unleash", "feature toggle", From ecfa80746b89474dfcc0b4caf788f8751b80ece3 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sat, 26 Feb 2022 00:01:48 +0000 Subject: [PATCH 22/67] chore(deps): update dependency eslint to v8.10.0 --- package.json | 2 +- yarn.lock | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index e5a7246ca2..6504dfb3f6 100644 --- a/package.json +++ b/package.json @@ -138,7 +138,7 @@ "copyfiles": "2.4.1", "coveralls": "3.1.1", "del-cli": "4.0.1", - "eslint": "8.9.0", + "eslint": "8.10.0", "eslint-config-airbnb-base": "15.0.0", "eslint-config-airbnb-typescript": "16.0.0", "eslint-config-prettier": "8.4.0", diff --git a/yarn.lock b/yarn.lock index cf7feb0e4e..aaa6ca5150 100644 --- a/yarn.lock +++ b/yarn.lock @@ -621,10 +621,10 @@ dependencies: "@cspotcode/source-map-consumer" "0.8.0" -"@eslint/eslintrc@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.1.0.tgz#583d12dbec5d4f22f333f9669f7d0b7c7815b4d3" - integrity sha512-C1DfL7XX4nPqGd6jcP01W9pVM1HYCuUkFk1432D7F0v3JSlUIeOYn9oCoi3eoLZ+iwBSb29BMFxxny0YrrEZqg== +"@eslint/eslintrc@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.2.0.tgz#7ce1547a5c46dfe56e1e45c3c9ed18038c721c6a" + integrity sha512-igm9SjJHNEJRiUnecP/1R5T3wKLEJ7pL6e2P+GUSfCd0dGjPYYZve08uzw8L2J8foVHFz+NGu12JxRcU2gGo6w== dependencies: ajv "^6.12.4" debug "^4.3.2" @@ -2816,12 +2816,12 @@ eslint-visitor-keys@^3.3.0: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== -eslint@8.9.0: - version "8.9.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.9.0.tgz#a2a8227a99599adc4342fd9b854cb8d8d6412fdb" - integrity sha512-PB09IGwv4F4b0/atrbcMFboF/giawbBLVC7fyDamk5Wtey4Jh2K+rYaBhCAbUyEI4QzB1ly09Uglc9iCtFaG2Q== +eslint@8.10.0: + version "8.10.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.10.0.tgz#931be395eb60f900c01658b278e05b6dae47199d" + integrity sha512-tcI1D9lfVec+R4LE1mNDnzoJ/f71Kl/9Cv4nG47jOueCMBrCCKYXr4AUVS7go6mWYGFD4+EoN6+eXSrEbRzXVw== dependencies: - "@eslint/eslintrc" "^1.1.0" + "@eslint/eslintrc" "^1.2.0" "@humanwhocodes/config-array" "^0.9.2" ajv "^6.10.0" chalk "^4.0.0" From 4c34757f01e84cd39a51178b77a7a54930467ae7 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Mon, 28 Feb 2022 20:25:24 +0000 Subject: [PATCH 23/67] chore(deps): update typescript-eslint monorepo to v5.13.0 --- package.json | 4 +-- yarn.lock | 94 ++++++++++++++++++++++++++-------------------------- 2 files changed, 49 insertions(+), 49 deletions(-) diff --git a/package.json b/package.json index 6504dfb3f6..93b0fa6453 100644 --- a/package.json +++ b/package.json @@ -133,8 +133,8 @@ "@types/supertest": "2.0.11", "@types/type-is": "1.6.3", "@types/uuid": "8.3.4", - "@typescript-eslint/eslint-plugin": "5.12.1", - "@typescript-eslint/parser": "5.12.1", + "@typescript-eslint/eslint-plugin": "5.13.0", + "@typescript-eslint/parser": "5.13.0", "copyfiles": "2.4.1", "coveralls": "3.1.1", "del-cli": "4.0.1", diff --git a/yarn.lock b/yarn.lock index aaa6ca5150..50d1da84d8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1215,14 +1215,14 @@ dependencies: "@types/yargs-parser" "*" -"@typescript-eslint/eslint-plugin@5.12.1": - version "5.12.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.12.1.tgz#b2cd3e288f250ce8332d5035a2ff65aba3374ac4" - integrity sha512-M499lqa8rnNK7mUv74lSFFttuUsubIRdAbHcVaP93oFcKkEmHmLqy2n7jM9C8DVmFMYK61ExrZU6dLYhQZmUpw== +"@typescript-eslint/eslint-plugin@5.13.0": + version "5.13.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.13.0.tgz#2809052b85911ced9c54a60dac10e515e9114497" + integrity sha512-vLktb2Uec81fxm/cfz2Hd6QaWOs8qdmVAZXLdOBX6JFJDhf6oDZpMzZ4/LZ6SFM/5DgDcxIMIvy3F+O9yZBuiQ== dependencies: - "@typescript-eslint/scope-manager" "5.12.1" - "@typescript-eslint/type-utils" "5.12.1" - "@typescript-eslint/utils" "5.12.1" + "@typescript-eslint/scope-manager" "5.13.0" + "@typescript-eslint/type-utils" "5.13.0" + "@typescript-eslint/utils" "5.13.0" debug "^4.3.2" functional-red-black-tree "^1.0.1" ignore "^5.1.8" @@ -1230,69 +1230,69 @@ semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/parser@5.12.1": - version "5.12.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.12.1.tgz#b090289b553b8aa0899740d799d0f96e6f49771b" - integrity sha512-6LuVUbe7oSdHxUWoX/m40Ni8gsZMKCi31rlawBHt7VtW15iHzjbpj2WLiToG2758KjtCCiLRKZqfrOdl3cNKuw== +"@typescript-eslint/parser@5.13.0": + version "5.13.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.13.0.tgz#0394ed8f2f849273c0bf4b811994d177112ced5c" + integrity sha512-GdrU4GvBE29tm2RqWOM0P5QfCtgCyN4hXICj/X9ibKED16136l9ZpoJvCL5pSKtmJzA+NRDzQ312wWMejCVVfg== dependencies: - "@typescript-eslint/scope-manager" "5.12.1" - "@typescript-eslint/types" "5.12.1" - "@typescript-eslint/typescript-estree" "5.12.1" + "@typescript-eslint/scope-manager" "5.13.0" + "@typescript-eslint/types" "5.13.0" + "@typescript-eslint/typescript-estree" "5.13.0" debug "^4.3.2" -"@typescript-eslint/scope-manager@5.12.1": - version "5.12.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.12.1.tgz#58734fd45d2d1dec49641aacc075fba5f0968817" - integrity sha512-J0Wrh5xS6XNkd4TkOosxdpObzlYfXjAFIm9QxYLCPOcHVv1FyyFCPom66uIh8uBr0sZCrtS+n19tzufhwab8ZQ== +"@typescript-eslint/scope-manager@5.13.0": + version "5.13.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.13.0.tgz#cf6aff61ca497cb19f0397eea8444a58f46156b6" + integrity sha512-T4N8UvKYDSfVYdmJq7g2IPJYCRzwtp74KyDZytkR4OL3NRupvswvmJQJ4CX5tDSurW2cvCc1Ia1qM7d0jpa7IA== dependencies: - "@typescript-eslint/types" "5.12.1" - "@typescript-eslint/visitor-keys" "5.12.1" + "@typescript-eslint/types" "5.13.0" + "@typescript-eslint/visitor-keys" "5.13.0" -"@typescript-eslint/type-utils@5.12.1": - version "5.12.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.12.1.tgz#8d58c6a0bb176b5e9a91581cda1a7f91a114d3f0" - integrity sha512-Gh8feEhsNLeCz6aYqynh61Vsdy+tiNNkQtc+bN3IvQvRqHkXGUhYkUi+ePKzP0Mb42se7FDb+y2SypTbpbR/Sg== +"@typescript-eslint/type-utils@5.13.0": + version "5.13.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.13.0.tgz#b0efd45c85b7bab1125c97b752cab3a86c7b615d" + integrity sha512-/nz7qFizaBM1SuqAKb7GLkcNn2buRdDgZraXlkhz+vUGiN1NZ9LzkA595tHHeduAiS2MsHqMNhE2zNzGdw43Yg== dependencies: - "@typescript-eslint/utils" "5.12.1" + "@typescript-eslint/utils" "5.13.0" debug "^4.3.2" tsutils "^3.21.0" -"@typescript-eslint/types@5.12.1": - version "5.12.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.12.1.tgz#46a36a28ff4d946821b58fe5a73c81dc2e12aa89" - integrity sha512-hfcbq4qVOHV1YRdhkDldhV9NpmmAu2vp6wuFODL71Y0Ixak+FLeEU4rnPxgmZMnGreGEghlEucs9UZn5KOfHJA== +"@typescript-eslint/types@5.13.0": + version "5.13.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.13.0.tgz#da1de4ae905b1b9ff682cab0bed6b2e3be9c04e5" + integrity sha512-LmE/KO6DUy0nFY/OoQU0XelnmDt+V8lPQhh8MOVa7Y5k2gGRd6U9Kp3wAjhB4OHg57tUO0nOnwYQhRRyEAyOyg== -"@typescript-eslint/typescript-estree@5.12.1": - version "5.12.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.12.1.tgz#6a9425b9c305bcbc38e2d1d9a24c08e15e02b722" - integrity sha512-ahOdkIY9Mgbza7L9sIi205Pe1inCkZWAHE1TV1bpxlU4RZNPtXaDZfiiFWcL9jdxvW1hDYZJXrFm+vlMkXRbBw== +"@typescript-eslint/typescript-estree@5.13.0": + version "5.13.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.13.0.tgz#b37c07b748ff030a3e93d87c842714e020b78141" + integrity sha512-Q9cQow0DeLjnp5DuEDjLZ6JIkwGx3oYZe+BfcNuw/POhtpcxMTy18Icl6BJqTSd+3ftsrfuVb7mNHRZf7xiaNA== dependencies: - "@typescript-eslint/types" "5.12.1" - "@typescript-eslint/visitor-keys" "5.12.1" + "@typescript-eslint/types" "5.13.0" + "@typescript-eslint/visitor-keys" "5.13.0" debug "^4.3.2" globby "^11.0.4" is-glob "^4.0.3" semver "^7.3.5" tsutils "^3.21.0" -"@typescript-eslint/utils@5.12.1": - version "5.12.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.12.1.tgz#447c24a05d9c33f9c6c64cb48f251f2371eef920" - integrity sha512-Qq9FIuU0EVEsi8fS6pG+uurbhNTtoYr4fq8tKjBupsK5Bgbk2I32UGm0Sh+WOyjOPgo/5URbxxSNV6HYsxV4MQ== +"@typescript-eslint/utils@5.13.0": + version "5.13.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.13.0.tgz#2328feca700eb02837298339a2e49c46b41bd0af" + integrity sha512-+9oHlPWYNl6AwwoEt5TQryEHwiKRVjz7Vk6kaBeD3/kwHE5YqTGHtm/JZY8Bo9ITOeKutFaXnBlMgSATMJALUQ== dependencies: "@types/json-schema" "^7.0.9" - "@typescript-eslint/scope-manager" "5.12.1" - "@typescript-eslint/types" "5.12.1" - "@typescript-eslint/typescript-estree" "5.12.1" + "@typescript-eslint/scope-manager" "5.13.0" + "@typescript-eslint/types" "5.13.0" + "@typescript-eslint/typescript-estree" "5.13.0" eslint-scope "^5.1.1" eslint-utils "^3.0.0" -"@typescript-eslint/visitor-keys@5.12.1": - version "5.12.1" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.12.1.tgz#f722da106c8f9695ae5640574225e45af3e52ec3" - integrity sha512-l1KSLfupuwrXx6wc0AuOmC7Ko5g14ZOQ86wJJqRbdLbXLK02pK/DPiDDqCc7BqqiiA04/eAA6ayL0bgOrAkH7A== +"@typescript-eslint/visitor-keys@5.13.0": + version "5.13.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.13.0.tgz#f45ff55bcce16403b221ac9240fbeeae4764f0fd" + integrity sha512-HLKEAS/qA1V7d9EzcpLFykTePmOQqOFim8oCvhY3pZgQ8Hi38hYpHd9e5GN6nQBFQNecNhws5wkS9Y5XIO0s/g== dependencies: - "@typescript-eslint/types" "5.12.1" + "@typescript-eslint/types" "5.13.0" eslint-visitor-keys "^3.0.0" abab@^2.0.3, abab@^2.0.5: From 49f1a9f03e45c804b9cfd503c9b34d0bd6eed470 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Tue, 1 Mar 2022 02:50:40 +0000 Subject: [PATCH 24/67] chore(deps): update dependency typescript to v4.6.2 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 93b0fa6453..df7483a44f 100644 --- a/package.json +++ b/package.json @@ -158,7 +158,7 @@ "ts-jest": "27.1.3", "ts-node": "10.5.0", "tsc-watch": "4.6.0", - "typescript": "4.5.5" + "typescript": "4.6.2" }, "resolutions": { "db-migrate/rc/minimist": "^1.2.5", diff --git a/yarn.lock b/yarn.lock index 50d1da84d8..331ca6e7a4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7192,10 +7192,10 @@ typedarray@^0.0.6: resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@4.5.5: - version "4.5.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3" - integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA== +typescript@4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.2.tgz#fe12d2727b708f4eef40f51598b3398baa9611d4" + integrity sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg== uid-safe@~2.1.5: version "2.1.5" From fc4d95ff5bd76a463131ba81164359a9a645f9fa Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Tue, 1 Mar 2022 10:52:22 +0100 Subject: [PATCH 25/67] fix: configure user endpoint when AuthType is NONE (#1403) Co-authored-by: Fredrik Oseberg --- src/lib/middleware/no-authentication.ts | 10 +++------- src/lib/routes/admin-api/config.ts | 6 ++++-- src/lib/routes/admin-api/user.ts | 13 ++++++++----- src/lib/types/no-auth-user.ts | 22 ++++++++++++++++++++++ 4 files changed, 37 insertions(+), 14 deletions(-) create mode 100644 src/lib/types/no-auth-user.ts diff --git a/src/lib/middleware/no-authentication.ts b/src/lib/middleware/no-authentication.ts index 2696b09b86..3d7eaacc65 100644 --- a/src/lib/middleware/no-authentication.ts +++ b/src/lib/middleware/no-authentication.ts @@ -1,16 +1,12 @@ import { Application } from 'express'; -import { ADMIN } from '../types/permissions'; -import ApiUser from '../types/api-user'; +import NoAuthUser from '../types/no-auth-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], - }); + // @ts-expect-error + req.user = new NoAuthUser(); } next(); }); diff --git a/src/lib/routes/admin-api/config.ts b/src/lib/routes/admin-api/config.ts index fbc4867d7e..16298da06a 100644 --- a/src/lib/routes/admin-api/config.ts +++ b/src/lib/routes/admin-api/config.ts @@ -1,6 +1,6 @@ import { Request, Response } from 'express'; import { IUnleashServices } from '../../types/services'; -import { IUnleashConfig } from '../../types/option'; +import { IAuthType, IUnleashConfig } from '../../types/option'; import version from '../../util/version'; import Controller from '../controller'; @@ -46,7 +46,9 @@ class ConfigController extends Controller { await this.settingService.get(simpleAuthKey); const versionInfo = this.versionService.getVersionInfo(); - const disablePasswordAuth = simpleAuthSettings?.disabled; + const disablePasswordAuth = + simpleAuthSettings?.disabled || + this.config.authentication.type == IAuthType.NONE; res.json({ ...config, versionInfo, disablePasswordAuth }); } } diff --git a/src/lib/routes/admin-api/user.ts b/src/lib/routes/admin-api/user.ts index ef98e01e53..c1104d869d 100644 --- a/src/lib/routes/admin-api/user.ts +++ b/src/lib/routes/admin-api/user.ts @@ -2,13 +2,13 @@ import { Response } from 'express'; import { IAuthRequest } from '../unleash-types'; import Controller from '../controller'; import { AccessService } from '../../services/access-service'; -import { IUnleashConfig } from '../../types/option'; +import { IAuthType, IUnleashConfig } from '../../types/option'; import { IUnleashServices } from '../../types/services'; import UserService from '../../services/user-service'; import SessionService from '../../services/session-service'; import UserFeedbackService from '../../services/user-feedback-service'; import UserSplashService from '../../services/user-splash-service'; -import { NONE } from '../../types/permissions'; +import { ADMIN, NONE } from '../../types/permissions'; interface IChangeUserRequest { password: string; @@ -58,9 +58,12 @@ class UserController extends Controller { async getUser(req: IAuthRequest, res: Response): Promise { res.setHeader('cache-control', 'no-store'); const { user } = req; - const permissions = await this.accessService.getPermissionsForUser( - user, - ); + let permissions; + if (this.config.authentication.type === IAuthType.NONE) { + permissions = [{ permission: ADMIN }]; + } else { + permissions = await this.accessService.getPermissionsForUser(user); + } const feedback = await this.userFeedbackService.getAllUserFeedback( user, ); diff --git a/src/lib/types/no-auth-user.ts b/src/lib/types/no-auth-user.ts new file mode 100644 index 0000000000..fbb65e8cde --- /dev/null +++ b/src/lib/types/no-auth-user.ts @@ -0,0 +1,22 @@ +import { ADMIN } from './permissions'; + +export default class NoAuthUser { + isAPI: boolean; + + username: string; + + id: number; + + permissions: string[]; + + constructor( + username: string = 'unknown', + id: number = -1, + permissions: string[] = [ADMIN], + ) { + this.isAPI = true; + this.username = username; + this.id = id; + this.permissions = permissions; + } +} From 8651ff2cd5f44847420ef67b034a75202af4a136 Mon Sep 17 00:00:00 2001 From: Christopher Kolstad Date: Tue, 1 Mar 2022 12:51:33 +0100 Subject: [PATCH 26/67] 4.8.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index df7483a44f..028b00fb47 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "unleash-server", "description": "Unleash is an enterprise ready feature toggles service. It provides different strategies for handling feature toggles.", - "version": "4.8.1", + "version": "4.8.2", "keywords": [ "unleash", "feature toggle", From 995b0eda0331c1e156e091270f1ee06378cf8bbb Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Tue, 1 Mar 2022 18:07:02 +0000 Subject: [PATCH 27/67] chore(deps): update dependency ts-node to v10.6.0 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 028b00fb47..a0b8964de4 100644 --- a/package.json +++ b/package.json @@ -156,7 +156,7 @@ "superagent": "7.1.1", "supertest": "6.2.2", "ts-jest": "27.1.3", - "ts-node": "10.5.0", + "ts-node": "10.6.0", "tsc-watch": "4.6.0", "typescript": "4.6.2" }, diff --git a/yarn.lock b/yarn.lock index 331ca6e7a4..af0835cc47 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7040,10 +7040,10 @@ ts-jest@27.1.3: semver "7.x" yargs-parser "20.x" -ts-node@10.5.0: - version "10.5.0" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.5.0.tgz#618bef5854c1fbbedf5e31465cbb224a1d524ef9" - integrity sha512-6kEJKwVxAJ35W4akuiysfKwKmjkbYxwQMTBaAxo9KKAx/Yd26mPUyhGz3ji+EsJoAgrLqVsYHNuuYwQe22lbtw== +ts-node@10.6.0: + version "10.6.0" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.6.0.tgz#c3f4195d5173ce3affdc8f2fd2e9a7ac8de5376a" + integrity sha512-CJen6+dfOXolxudBQXnVjRVvYTmTWbyz7cn+xq2XTsvnaXbHqr4gXSCNbS2Jj8yTZMuGwUoBESLaOkLascVVvg== dependencies: "@cspotcode/source-map-support" "0.7.0" "@tsconfig/node10" "^1.0.7" From d56611675f23ee08c7e7032756f3fc336e91ac88 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Wed, 2 Mar 2022 21:22:19 +0000 Subject: [PATCH 28/67] chore(deps): update dependency eslint-config-prettier to v8.5.0 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index a0b8964de4..38d6cc334a 100644 --- a/package.json +++ b/package.json @@ -141,7 +141,7 @@ "eslint": "8.10.0", "eslint-config-airbnb-base": "15.0.0", "eslint-config-airbnb-typescript": "16.0.0", - "eslint-config-prettier": "8.4.0", + "eslint-config-prettier": "8.5.0", "eslint-plugin-import": "2.25.4", "eslint-plugin-prettier": "4.0.0", "faker": "5.5.3", diff --git a/yarn.lock b/yarn.lock index af0835cc47..9a48dbfdd3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2731,10 +2731,10 @@ eslint-config-airbnb-typescript@16.0.0: dependencies: eslint-config-airbnb-base "^15.0.0" -eslint-config-prettier@8.4.0: - version "8.4.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.4.0.tgz#8e6d17c7436649e98c4c2189868562921ef563de" - integrity sha512-CFotdUcMY18nGRo5KGsnNxpznzhkopOcOo0InID+sgQssPrzjvsyKZPvOgymTFeHrFuC3Tzdf2YndhXtULK9Iw== +eslint-config-prettier@8.5.0: + version "8.5.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz#5a81680ec934beca02c7b1a61cf8ca34b66feab1" + integrity sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q== eslint-import-resolver-node@^0.3.6: version "0.3.6" From 8364c3b396bf01b15b606484e268f446dfbf86f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20Conradi=20=C3=98sthus?= Date: Mon, 21 Feb 2022 14:39:59 +0100 Subject: [PATCH 29/67] fix: add method to change role for project memeber --- src/lib/db/access-store.ts | 13 +++ src/lib/services/access-service.ts | 8 ++ src/lib/services/project-service.ts | 91 +++++++++++++++---- src/lib/types/events.ts | 22 +++++ src/lib/types/stores/access-store.ts | 6 ++ .../e2e/services/project-service.e2e.test.ts | 61 +++++++++++++ src/test/fixtures/fake-access-store.ts | 8 ++ 7 files changed, 191 insertions(+), 18 deletions(-) diff --git a/src/lib/db/access-store.ts b/src/lib/db/access-store.ts index b60ed3982f..1649c82d2c 100644 --- a/src/lib/db/access-store.ts +++ b/src/lib/db/access-store.ts @@ -247,6 +247,19 @@ export class AccessStore implements IAccessStore { .delete(); } + async updateUserProjectRole( + userId: number, + roleId: number, + projectId: string, + ): Promise { + return this.db(T.ROLE_USER) + .where({ + user_id: userId, + project: projectId, + }) + .update('role_id', roleId); + } + async removeRolesOfTypeForUser( userId: number, roleType: string, diff --git a/src/lib/services/access-service.ts b/src/lib/services/access-service.ts index 7be15b9677..065c2b34cc 100644 --- a/src/lib/services/access-service.ts +++ b/src/lib/services/access-service.ts @@ -220,6 +220,14 @@ export class AccessService { return this.store.removeUserFromRole(userId, roleId, projectId); } + async updateUserProjectRole( + userId: number, + roleId: number, + projectId: string, + ): Promise { + return this.store.updateUserProjectRole(userId, roleId, projectId); + } + //This actually only exists for testing purposes async addPermissionToRole( roleId: number, diff --git a/src/lib/services/project-service.ts b/src/lib/services/project-service.ts index 01605f1e61..8281a0fb2e 100644 --- a/src/lib/services/project-service.ts +++ b/src/lib/services/project-service.ts @@ -8,6 +8,7 @@ import NotFoundError from '../error/notfound-error'; import { ProjectUserAddedEvent, ProjectUserRemovedEvent, + ProjectUserUpdateRoleEvent, PROJECT_CREATED, PROJECT_DELETED, PROJECT_UPDATED, @@ -309,23 +310,9 @@ export default class ProjectService { userId: number, createdBy?: string, ): Promise { - const roles = await this.accessService.getRolesForProject(projectId); - const role = roles.find((r) => r.id === roleId); - if (!role) { - throw new NotFoundError( - `Couldn't find roleId=${roleId} on project=${projectId}`, - ); - } + const role = await this.findProjectRole(projectId, roleId); - if (role.name === RoleName.OWNER) { - const users = await this.accessService.getProjectUsersForRole( - role.id, - projectId, - ); - if (users.length < 2) { - throw new Error('A project must have at least one owner'); - } - } + await this.validateAtLeastOneOwner(projectId, role); await this.accessService.removeUserFromRole(userId, role.id, projectId); @@ -338,6 +325,76 @@ export default class ProjectService { ); } + async findProjectRole( + projectId: string, + roleId: number, + ): Promise { + const roles = await this.accessService.getRolesForProject(projectId); + const role = roles.find((r) => r.id === roleId); + if (!role) { + throw new NotFoundError( + `Couldn't find roleId=${roleId} on project=${projectId}`, + ); + } + return role; + } + + async validateAtLeastOneOwner( + projectId: string, + currentRole: IRoleDescriptor, + ): Promise { + if (currentRole.name === RoleName.OWNER) { + const users = await this.accessService.getProjectUsersForRole( + currentRole.id, + projectId, + ); + if (users.length < 2) { + throw new Error('A project must have at least one owner'); + } + } + } + + async changeRole( + projectId: string, + roleId: number, + userId: number, + createdBy: string, + ): Promise { + const role = await this.findProjectRole(projectId, roleId); + + const usersWithRoles = await this.getUsersWithAccess(projectId); + const user = usersWithRoles.users.find((u) => u.id === userId); + const currentRole = usersWithRoles.roles.find( + (r) => r.id === user.roleId, + ); + + if (currentRole.id === roleId) { + // Nothing to do.... + return; + } + + await this.validateAtLeastOneOwner(projectId, currentRole); + + await this.accessService.updateUserProjectRole( + userId, + roleId, + projectId, + ); + + await this.eventStore.store( + new ProjectUserUpdateRoleEvent({ + project: projectId, + createdBy, + preData: { + userId, + roleId: currentRole.id, + roleName: currentRole.name, + }, + data: { userId, roleId, roleName: role.name }, + }), + ); + } + async getMembers(projectId: string): Promise { return this.store.getMembers(projectId); } @@ -366,5 +423,3 @@ export default class ProjectService { }; } } - -module.exports = ProjectService; diff --git a/src/lib/types/events.ts b/src/lib/types/events.ts index bdf8b37bc1..2f5b32b6d3 100644 --- a/src/lib/types/events.ts +++ b/src/lib/types/events.ts @@ -41,6 +41,7 @@ export const PROJECT_DELETED = 'project-deleted'; export const PROJECT_IMPORT = 'project-import'; export const PROJECT_USER_ADDED = 'project-user-added'; export const PROJECT_USER_REMOVED = 'project-user-removed'; +export const PROJECT_USER_ROLE_CHANGED = 'project-user-role-changed'; export const DROP_PROJECTS = 'drop-projects'; export const TAG_CREATED = 'tag-created'; export const TAG_DELETED = 'tag-deleted'; @@ -412,3 +413,24 @@ export class ProjectUserRemovedEvent extends BaseEvent { this.preData = preData; } } + +export class ProjectUserUpdateRoleEvent extends BaseEvent { + readonly project: string; + + readonly data: any; + + readonly preData: any; + + constructor(p: { + project: string; + createdBy: string; + data: any; + preData: any; + }) { + super(PROJECT_USER_REMOVED, p.createdBy); + const { project, data, preData } = p; + this.project = project; + this.data = data; + this.preData = preData; + } +} diff --git a/src/lib/types/stores/access-store.ts b/src/lib/types/stores/access-store.ts index bac68b07c0..dd63603f7a 100644 --- a/src/lib/types/stores/access-store.ts +++ b/src/lib/types/stores/access-store.ts @@ -19,6 +19,7 @@ export interface IRoleWithPermissions extends IRole { } export interface IRoleDescriptor { + id: number; name: string; description?: string; type: string; @@ -51,6 +52,11 @@ export interface IAccessStore extends Store { roleId: number, projectId?: string, ): Promise; + updateUserProjectRole( + userId: number, + roleId: number, + projectId: string, + ): Promise; removeRolesOfTypeForUser(userId: number, roleType: string): Promise; addPermissionsToRole( role_id: number, diff --git a/src/test/e2e/services/project-service.e2e.test.ts b/src/test/e2e/services/project-service.e2e.test.ts index fac909c19e..2f72fc3e1b 100644 --- a/src/test/e2e/services/project-service.e2e.test.ts +++ b/src/test/e2e/services/project-service.e2e.test.ts @@ -652,3 +652,64 @@ test('should change a users role in the project', async () => { expect(customUser[0].id).toBe(projectUser.id); expect(customUser[0].name).toBe(projectUser.name); }); + +test('should update role for user on project', async () => { + const project = { + id: 'update-users', + name: 'New project', + description: 'Blah', + }; + await projectService.createProject(project, user); + + const projectMember1 = await stores.userStore.insert({ + name: 'Some Member', + email: 'update99@getunleash.io', + }); + + const memberRole = await stores.roleStore.getRoleByName(RoleName.MEMBER); + const ownerRole = await stores.roleStore.getRoleByName(RoleName.OWNER); + + await projectService.addUser(project.id, memberRole.id, projectMember1.id); + await projectService.changeRole( + project.id, + ownerRole.id, + projectMember1.id, + 'test', + ); + + const { users } = await projectService.getUsersWithAccess(project.id, user); + const memberUsers = users.filter((u) => u.roleId === memberRole.id); + const ownerUsers = users.filter((u) => u.roleId === ownerRole.id); + + expect(memberUsers).toHaveLength(0); + expect(ownerUsers).toHaveLength(2); +}); + +test('should not update role for user on project when she is the owner', async () => { + const project = { + id: 'update-users-not-allowed', + name: 'New project', + description: 'Blah', + }; + await projectService.createProject(project, user); + + const projectMember1 = await stores.userStore.insert({ + name: 'Some Member', + email: 'update991@getunleash.io', + }); + + const memberRole = await stores.roleStore.getRoleByName(RoleName.MEMBER); + + await projectService.addUser(project.id, memberRole.id, projectMember1.id); + + await expect(async () => { + await projectService.changeRole( + project.id, + memberRole.id, + user.id, + 'test', + ); + }).rejects.toThrowError( + new Error('A project must have at least one owner'), + ); +}); diff --git a/src/test/fixtures/fake-access-store.ts b/src/test/fixtures/fake-access-store.ts index cc50d1dd18..0194ed18ae 100644 --- a/src/test/fixtures/fake-access-store.ts +++ b/src/test/fixtures/fake-access-store.ts @@ -9,6 +9,14 @@ import { import { IAvailablePermissions, IPermission } from 'lib/types/model'; class AccessStoreMock implements IAccessStore { + updateUserProjectRole( + userId: number, + roleId: number, + projectId: string, + ): Promise { + throw new Error('Method not implemented.'); + } + removeUserFromRole( userId: number, roleId: number, From 2f189db3945bfb61cabdf21b1c41d7412021fa4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20Conradi=20=C3=98sthus?= Date: Thu, 24 Feb 2022 14:58:54 +0100 Subject: [PATCH 30/67] Update src/lib/services/project-service.ts Co-authored-by: Fredrik Strand Oseberg --- src/lib/services/project-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/services/project-service.ts b/src/lib/services/project-service.ts index 8281a0fb2e..6b2ae30196 100644 --- a/src/lib/services/project-service.ts +++ b/src/lib/services/project-service.ts @@ -363,7 +363,7 @@ export default class ProjectService { const role = await this.findProjectRole(projectId, roleId); const usersWithRoles = await this.getUsersWithAccess(projectId); - const user = usersWithRoles.users.find((u) => u.id === userId); + const user = usersWithRoles.users.find((user) => user.id === userId); const currentRole = usersWithRoles.roles.find( (r) => r.id === user.roleId, ); From 017bd75c9cd0967ce7678700bf094dd122b40ff5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20Conradi=20=C3=98sthus?= Date: Thu, 24 Feb 2022 14:59:01 +0100 Subject: [PATCH 31/67] Update src/lib/services/project-service.ts Co-authored-by: Fredrik Strand Oseberg --- src/lib/services/project-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/services/project-service.ts b/src/lib/services/project-service.ts index 6b2ae30196..6ad953a2e8 100644 --- a/src/lib/services/project-service.ts +++ b/src/lib/services/project-service.ts @@ -365,7 +365,7 @@ export default class ProjectService { const usersWithRoles = await this.getUsersWithAccess(projectId); const user = usersWithRoles.users.find((user) => user.id === userId); const currentRole = usersWithRoles.roles.find( - (r) => r.id === user.roleId, + (role) => role.id === user.roleId, ); if (currentRole.id === roleId) { From f0abc0e0d605f7e6e9d34c1c286bb78b663e6f83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20Conradi=20=C3=98sthus?= Date: Thu, 24 Feb 2022 14:59:06 +0100 Subject: [PATCH 32/67] Update src/lib/types/events.ts Co-authored-by: Fredrik Strand Oseberg --- src/lib/types/events.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/types/events.ts b/src/lib/types/events.ts index 2f5b32b6d3..f5d1f015ed 100644 --- a/src/lib/types/events.ts +++ b/src/lib/types/events.ts @@ -421,7 +421,7 @@ export class ProjectUserUpdateRoleEvent extends BaseEvent { readonly preData: any; - constructor(p: { + constructor(eventData: { project: string; createdBy: string; data: any; From 7f391e2b48f5762033b0df5a11fb18fda1ef78c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20Conradi=20=C3=98sthus?= Date: Thu, 24 Feb 2022 14:59:11 +0100 Subject: [PATCH 33/67] Update src/lib/types/events.ts Co-authored-by: Fredrik Strand Oseberg --- src/lib/types/events.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/types/events.ts b/src/lib/types/events.ts index f5d1f015ed..97646c6737 100644 --- a/src/lib/types/events.ts +++ b/src/lib/types/events.ts @@ -427,7 +427,7 @@ export class ProjectUserUpdateRoleEvent extends BaseEvent { data: any; preData: any; }) { - super(PROJECT_USER_REMOVED, p.createdBy); + super(PROJECT_USER_REMOVED, eventData.createdBy); const { project, data, preData } = p; this.project = project; this.data = data; From faa87469ff32f0ee90f18a0bc9e9b2a0a0766de9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20Conradi=20=C3=98sthus?= Date: Thu, 24 Feb 2022 14:59:16 +0100 Subject: [PATCH 34/67] Update src/lib/types/events.ts Co-authored-by: Fredrik Strand Oseberg --- src/lib/types/events.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/types/events.ts b/src/lib/types/events.ts index 97646c6737..57e700ea49 100644 --- a/src/lib/types/events.ts +++ b/src/lib/types/events.ts @@ -428,7 +428,7 @@ export class ProjectUserUpdateRoleEvent extends BaseEvent { preData: any; }) { super(PROJECT_USER_REMOVED, eventData.createdBy); - const { project, data, preData } = p; + const { project, data, preData } = eventData; this.project = project; this.data = data; this.preData = preData; From 8a4984fee61fac06ff64d8a544b34ad2969ce0ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20Conradi=20=C3=98sthus?= Date: Thu, 24 Feb 2022 14:59:21 +0100 Subject: [PATCH 35/67] Update src/test/e2e/services/project-service.e2e.test.ts Co-authored-by: Fredrik Strand Oseberg --- src/test/e2e/services/project-service.e2e.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/e2e/services/project-service.e2e.test.ts b/src/test/e2e/services/project-service.e2e.test.ts index 2f72fc3e1b..2b2de477e1 100644 --- a/src/test/e2e/services/project-service.e2e.test.ts +++ b/src/test/e2e/services/project-service.e2e.test.ts @@ -678,7 +678,7 @@ test('should update role for user on project', async () => { ); const { users } = await projectService.getUsersWithAccess(project.id, user); - const memberUsers = users.filter((u) => u.roleId === memberRole.id); + const memberUsers = users.filter((user) => user.roleId === memberRole.id); const ownerUsers = users.filter((u) => u.roleId === ownerRole.id); expect(memberUsers).toHaveLength(0); From 321c9a8492eacf42f9038de99642450e837eae94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20Conradi=20=C3=98sthus?= Date: Thu, 24 Feb 2022 14:59:26 +0100 Subject: [PATCH 36/67] Update src/test/e2e/services/project-service.e2e.test.ts Co-authored-by: Fredrik Strand Oseberg --- src/test/e2e/services/project-service.e2e.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/e2e/services/project-service.e2e.test.ts b/src/test/e2e/services/project-service.e2e.test.ts index 2b2de477e1..1599209f26 100644 --- a/src/test/e2e/services/project-service.e2e.test.ts +++ b/src/test/e2e/services/project-service.e2e.test.ts @@ -679,7 +679,7 @@ test('should update role for user on project', async () => { const { users } = await projectService.getUsersWithAccess(project.id, user); const memberUsers = users.filter((user) => user.roleId === memberRole.id); - const ownerUsers = users.filter((u) => u.roleId === ownerRole.id); + const ownerUsers = users.filter((user) => user.roleId === ownerRole.id); expect(memberUsers).toHaveLength(0); expect(ownerUsers).toHaveLength(2); From d514356030b6ea2915c5285c150f9154fb75d081 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20Conradi=20=C3=98sthus?= Date: Wed, 2 Mar 2022 23:48:43 +0100 Subject: [PATCH 37/67] fix: avoid scope issues --- src/lib/services/project-service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/services/project-service.ts b/src/lib/services/project-service.ts index 6ad953a2e8..8281a0fb2e 100644 --- a/src/lib/services/project-service.ts +++ b/src/lib/services/project-service.ts @@ -363,9 +363,9 @@ export default class ProjectService { const role = await this.findProjectRole(projectId, roleId); const usersWithRoles = await this.getUsersWithAccess(projectId); - const user = usersWithRoles.users.find((user) => user.id === userId); + const user = usersWithRoles.users.find((u) => u.id === userId); const currentRole = usersWithRoles.roles.find( - (role) => role.id === user.roleId, + (r) => r.id === user.roleId, ); if (currentRole.id === roleId) { From d264f30fa00469ee348b64761aa4a9166fe1646c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivar=20Conradi=20=C3=98sthus?= Date: Wed, 2 Mar 2022 23:53:46 +0100 Subject: [PATCH 38/67] fix: lint --- src/test/e2e/services/project-service.e2e.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/e2e/services/project-service.e2e.test.ts b/src/test/e2e/services/project-service.e2e.test.ts index 1599209f26..2f72fc3e1b 100644 --- a/src/test/e2e/services/project-service.e2e.test.ts +++ b/src/test/e2e/services/project-service.e2e.test.ts @@ -678,8 +678,8 @@ test('should update role for user on project', async () => { ); const { users } = await projectService.getUsersWithAccess(project.id, user); - const memberUsers = users.filter((user) => user.roleId === memberRole.id); - const ownerUsers = users.filter((user) => user.roleId === ownerRole.id); + const memberUsers = users.filter((u) => u.roleId === memberRole.id); + const ownerUsers = users.filter((u) => u.roleId === ownerRole.id); expect(memberUsers).toHaveLength(0); expect(ownerUsers).toHaveLength(2); From f92d86061a34998fcf7ec2f1554b28a5ac7178e6 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Tue, 1 Feb 2022 13:21:16 +0100 Subject: [PATCH 39/67] docs(chore): improve syntax highlighting and formatting of code bits Use `bash` instead of `sh` for nicer highlighting by prism. I've also broken large json blobs over multiple lines. --- .../docs/api/admin/feature-toggles-api-v2.md | 95 +++++++++++++------ 1 file changed, 65 insertions(+), 30 deletions(-) diff --git a/website/docs/api/admin/feature-toggles-api-v2.md b/website/docs/api/admin/feature-toggles-api-v2.md index f329396275..e52c8da374 100644 --- a/website/docs/api/admin/feature-toggles-api-v2.md +++ b/website/docs/api/admin/feature-toggles-api-v2.md @@ -24,7 +24,10 @@ This endpoint will give you an general overview of a project. It will return ess **Example Query** -`http GET http://localhost:4242/api/admin/projects/default Authorization:$KEY` +```bash +http GET http://localhost:4242/api/admin/projects/default Authorization:$KEY +``` + **Example response:** @@ -90,7 +93,11 @@ This endpoint will return all feature toggles and high level environment details **Example Query** -`http GET http://localhost:4242/api/admin/projects/default/features Authorization:$KEY` +``` bash +http GET http://localhost:4242/api/admin/projects/default/features \ +Authorization:$KEY +``` + **Example response:** @@ -162,7 +169,8 @@ This endpoint accepts the following toggle options: ```bash echo '{"name": "demo2", "description": "A new feature toggle"}' | \ -http POST http://localhost:4242/api/admin/projects/default/features Authorization:$KEY` +http POST http://localhost:4242/api/admin/projects/default/features \ +Authorization:$KEY` ``` @@ -205,8 +213,9 @@ This endpoint will return the feature toggles with the defined name and _project **Example Query** -```sh -http GET http://localhost:4242/api/admin/projects/default/features/demo Authorization:$KEY` +```bash +http GET http://localhost:4242/api/admin/projects/default/features/demo \ +Authorization:$KEY` ``` **Example response:** @@ -256,8 +265,10 @@ This endpoint will accept HTTP PUT request to update the feature toggle metadata **Example Query** -```sh -echo '{"name": "demo", "description": "An update feature toggle", "type": "kill-switch"}' | http PUT http://localhost:4242/api/admin/projects/default/features/demo Authorization:$KEY` +```bash +echo '{"name": "demo", "description": "An update feature toggle", "type": "kill-switch"}' | \ +http PUT http://localhost:4242/api/admin/projects/default/features/demo \ +Authorization:$KEY` ``` @@ -291,8 +302,10 @@ This endpoint will accept HTTP PATCH request to update the feature toggle metada **Example Query** -```sh -echo '[{"op": "replace", "path": "/description", "value": "patched desc"}]' | http PATCH http://localhost:4242/api/admin/projects/default/features/demo Authorization:$KEY` +```bash +echo '[{"op": "replace", "path": "/description", "value": "patched desc"}]' | \ +http PATCH http://localhost:4242/api/admin/projects/default/features/demo \ +Authorization:$KEY` ``` @@ -327,8 +340,10 @@ This endpoint will accept HTTP POST request to clone an existing feature toggle **Example Query** -```sh -echo '{ "name": "newName" }' | http POST http://localhost:4242/api/admin/projects/default/features/Demo/clone Authorization:$KEY` +```bash +echo '{ "name": "newName" }' | \ +http POST http://localhost:4242/api/admin/projects/default/features/Demo/clone \ +Authorization:$KEY` ``` @@ -379,21 +394,21 @@ This endpoint will accept HTTP PUT request to update the feature toggle metadata **Example Query** -```sh -http DELETE http://localhost:4242/api/admin/projects/default/features/demo Authorization:$KEY` +```bash +http DELETE http://localhost:4242/api/admin/projects/default/features/demo \ +Authorization:$KEY` ``` **Example response:** -```sh +``` HTTP/1.1 202 Accepted Access-Control-Allow-Origin: * Connection: keep-alive Date: Wed, 08 Sep 2021 20:09:21 GMT Keep-Alive: timeout=60 Transfer-Encoding: chunked - ``` @@ -405,9 +420,17 @@ This endpoint will allow you to add a new strategy to a feature toggle in a give **Example Query** -```sh - echo '{"name": "flexibleRollout", "parameters": { "rollout": 20, "groupId": "demo", "stickiness": "default" }}' | \ - http POST http://localhost:4242/api/admin/projects/default/features/demo/environments/production/strategies Authorization:$KEY +```bash +echo '{"name": "flexibleRollout", + "parameters": { + "rollout": 20, + "groupId": "demo", + "stickiness": "default" + } + }' | \ +http POST \ +http://localhost:4242/api/admin/projects/default/features/demo/environments/production/strategies \ +Authorization:$KEY ``` **Example response:** @@ -429,9 +452,17 @@ This endpoint will allow you to add a new strategy to a feature toggle in a give **Example Query** -```sh -echo '{"name": "flexibleRollout", "parameters": { "rollout": 25, "groupId": "demo","stickiness": "default" }}' | \ -http PUT http://localhost:4242/api/admin/projects/default/features/demo/environments/production/strategies/77bbe972-ffce-49b2-94d9-326593e2228e Authorization:$KEY +```bash +echo '{"name": "flexibleRollout", + "parameters": { + "rollout": 25, + "groupId": "demo", + "stickiness": "default" + } + }' | \ +http PUT \ +http://localhost:4242/api/admin/projects/default/features/demo/environments/production/strategies/77bbe972-ffce-49b2-94d9-326593e2228e \ +Authorization:$KEY ``` **Example response:** @@ -453,9 +484,11 @@ http PUT http://localhost:4242/api/admin/projects/default/features/demo/environm **Example Query** -```sh +```bash echo '[{"op": "replace", "path": "/parameters/rollout", "value": 50}]' | \ -http PATCH http://localhost:4242/api/admin/projects/default/features/demo/environments/production/strategies/ea5404e5-0c0d-488c-93b2-0a2200534827 Authorization:$KEY +http PATCH \ +http://localhost:4242/api/admin/projects/default/features/demo/environments/production/strategies/ea5404e5-0c0d-488c-93b2-0a2200534827 \ +Authorization:$KEY ``` **Example response:** @@ -478,13 +511,13 @@ http PATCH http://localhost:4242/api/admin/projects/default/features/demo/enviro **Example Query** -```sh +```bash http DELETE http://localhost:4242/api/admin/projects/default/features/demo/environments/production/strategies/77bbe972-ffce-49b2-94d9-326593e2228e Authorization:$KEY ``` **Example response:** -```sh +``` HTTP/1.1 200 OK Access-Control-Allow-Origin: * Connection: keep-alive @@ -499,13 +532,13 @@ Vary: Accept-Encoding **Example Query** -```sh +```bash http POST http://localhost:4242/api/admin/projects/default/features/demo/environments/development/on Authorization:$KEY --json ``` **Example response:** -```sh +``` HTTP/1.1 200 OK Access-Control-Allow-Origin: * Connection: keep-alive @@ -549,7 +582,7 @@ http PUT http://localhost:4242/api/admin/projects/default/features/demo/variants **Example response:** -```sh +```bash HTTP/1.1 200 OK Access-Control-Allow-Origin: * Connection: keep-alive @@ -579,13 +612,15 @@ Content-Type: application/json; charset=utf-8 **Example Query** -```sh +```bash echo '[{"op": "add", "path": "/1", "value": { "name": "new-variant", "weightType": "fix", "weight": 200 }}]' | \ -http PATCH http://localhost:4242/api/admin/projects/default/features/demo/variants Authorization:$KEY +http PATCH \ +http://localhost:4242/api/admin/projects/default/features/demo/variants \ +Authorization:$KEY ``` ** Example Response ** From 01f7293e2e5d6223e578e40c7e339484856cf0c5 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Tue, 1 Feb 2022 15:20:18 +0100 Subject: [PATCH 40/67] docs: add description of the project users/roles endpoint --- .../docs/api/admin/feature-toggles-api-v2.md | 128 ++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/website/docs/api/admin/feature-toggles-api-v2.md b/website/docs/api/admin/feature-toggles-api-v2.md index e52c8da374..411dc618ff 100644 --- a/website/docs/api/admin/feature-toggles-api-v2.md +++ b/website/docs/api/admin/feature-toggles-api-v2.md @@ -646,3 +646,131 @@ Authorization:$KEY ] } ``` + + +## Manage project users and roles + +You can add and remove users to a project using the `/api/admin/projects/:projectId/users/:userId/roles/:roleId` endpoint. When adding or removing users, you must also provide the ID for the role to give them (when adding) or the ID of the role they currently have (when removing). + +There is no way to update a user's role directly at the moment. Instead, if you want to change a user's role, first remove the user from the project and then add them back with the desired role. + +### Add a user to a project + +``` http +POST /api/admin/projects/:projectId/users/:userId/roles/:roleId +``` + +This will add a user to a project and give the user a specified role within that project. + +#### URL parameters + +| Parameter | Type | Description | Example value | +|-------------|---------|-----------------------------------------------------------------------|-------------------| +| `userId` | integer | The ID of the user you want to add to the project. | `1` | +| `projectId` | string | The id of the project to add the user to. | `"MyCoolProject"` | +| `roleId` | integer | The id of the role you want to assign to the new user in the project. | `7` | + + +#### Responses + +
+Responses data + +##### 200 OK + +The user was added to the project with the specified role. + +``` json +{ } +``` + +##### 400 Bad Request + +The user already exists in the project and cannot be added again: + +``` json +[ + { + "msg": "User already has access to project=" + } +] +``` + +
+ +#### Example query + +The following query would add the user with ID 42 to the _MyCoolProject_ project and give them the role with ID 13. + +```bash +http POST \ +http://localhost:4242/api/admin/projects/MyCoolProject/users/42/roles/13 \ +Authorization:$KEY +``` + +### Remove a user from a project + +``` http +DELETE /api/admin/projects/:projectId/users/:userId/roles/:roleId +``` + +This will remove the specified from user from the project. The user _must_ have the role indicated by the `:roleId` URL parameter for the request to succeed. + +#### URL parameters + +| Parameter | Type | Description | Example value | +|-------------|---------|-----------------------------------------------------------------------|-------------------| +| `userId` | integer | The ID of the user you want to remove from the project. | `1` | +| `projectId` | string | The id of the project to remove the user from. | `"MyCoolProject"` | +| `roleId` | integer | The current role the of the user you want to remove from the project. | `7` | + +#### Responses + +
+Responses data + +:::caution +If you provide the wrong role id for the user, but there is another user in the same project with the role you provided, you'll get a 200 OK response indicating success, but the user will not be removed. +::: + +##### 200 OK + +The user has been removed from the project. + +``` json +{ } +``` + +##### 400 Bad Request + +No user with the current role exists in the project: + + +``` json +[ + { + "msg": "Couldn't find roleId= on project=" + } +] +``` + +You tried to remove the only user with the role `owner` in the project: + +``` json +[ + { + "msg": "A project must have at least one owner." + } +] +``` + +
+ +#### Example query + +The following query would remove the user with ID 42 and role ID 13 from the _MyCoolProject_ project. +```bash +http DELETE \ +http://localhost:4242/api/admin/projects/MyCoolProject/users/42/roles/13 \ +Authorization:$KEY +``` From 81c6ef2d6ef66f4b95fd0a282cf8c76682f94d4e Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Tue, 1 Feb 2022 15:25:40 +0100 Subject: [PATCH 41/67] chore: align url parameters table. --- website/docs/api/admin/feature-toggles-api-v2.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/website/docs/api/admin/feature-toggles-api-v2.md b/website/docs/api/admin/feature-toggles-api-v2.md index 411dc618ff..8af81c3c3c 100644 --- a/website/docs/api/admin/feature-toggles-api-v2.md +++ b/website/docs/api/admin/feature-toggles-api-v2.md @@ -666,9 +666,9 @@ This will add a user to a project and give the user a specified role within that | Parameter | Type | Description | Example value | |-------------|---------|-----------------------------------------------------------------------|-------------------| -| `userId` | integer | The ID of the user you want to add to the project. | `1` | -| `projectId` | string | The id of the project to add the user to. | `"MyCoolProject"` | -| `roleId` | integer | The id of the role you want to assign to the new user in the project. | `7` | +| `userId` | integer | The ID of the user you want to add to the project. | `1` | +| `projectId` | string | The id of the project to add the user to. | `"MyCoolProject"` | +| `roleId` | integer | The id of the role you want to assign to the new user in the project. | `7` | #### Responses From 92349b5561d945a4b7106ed53b6ffd71f53227c2 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Wed, 23 Feb 2022 10:36:50 +0100 Subject: [PATCH 42/67] Apply suggestions from code review Co-authored-by: Christopher Kolstad --- website/docs/api/admin/feature-toggles-api-v2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/api/admin/feature-toggles-api-v2.md b/website/docs/api/admin/feature-toggles-api-v2.md index 8af81c3c3c..32cea43f3d 100644 --- a/website/docs/api/admin/feature-toggles-api-v2.md +++ b/website/docs/api/admin/feature-toggles-api-v2.md @@ -714,7 +714,7 @@ Authorization:$KEY DELETE /api/admin/projects/:projectId/users/:userId/roles/:roleId ``` -This will remove the specified from user from the project. The user _must_ have the role indicated by the `:roleId` URL parameter for the request to succeed. +This will remove the specified user from the project. The user _must_ have the role indicated by the `:roleId` URL parameter for the request to succeed. #### URL parameters From e84b8f30e1c189f776ae0ec5529f8f243682b07f Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Wed, 23 Feb 2022 10:39:13 +0100 Subject: [PATCH 43/67] docs: add correct return values for role deletion endpoints. --- website/docs/api/admin/feature-toggles-api-v2.md | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/website/docs/api/admin/feature-toggles-api-v2.md b/website/docs/api/admin/feature-toggles-api-v2.md index 32cea43f3d..62bceb58a3 100644 --- a/website/docs/api/admin/feature-toggles-api-v2.md +++ b/website/docs/api/admin/feature-toggles-api-v2.md @@ -678,11 +678,7 @@ This will add a user to a project and give the user a specified role within that ##### 200 OK -The user was added to the project with the specified role. - -``` json -{ } -``` +The user was added to the project with the specified role. This response has no body. ##### 400 Bad Request @@ -735,11 +731,7 @@ If you provide the wrong role id for the user, but there is another user in the ##### 200 OK -The user has been removed from the project. - -``` json -{ } -``` +The user has been removed from the project. This response has no body. ##### 400 Bad Request From 9da796eb90ac674c0d2dde3648cab2654721e05e Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Wed, 23 Feb 2022 10:50:02 +0100 Subject: [PATCH 44/67] docs: add details for change role endpoint. --- .../docs/api/admin/feature-toggles-api-v2.md | 95 ++++++++++++++++++- 1 file changed, 93 insertions(+), 2 deletions(-) diff --git a/website/docs/api/admin/feature-toggles-api-v2.md b/website/docs/api/admin/feature-toggles-api-v2.md index 62bceb58a3..a4b4d5c325 100644 --- a/website/docs/api/admin/feature-toggles-api-v2.md +++ b/website/docs/api/admin/feature-toggles-api-v2.md @@ -652,8 +652,6 @@ Authorization:$KEY You can add and remove users to a project using the `/api/admin/projects/:projectId/users/:userId/roles/:roleId` endpoint. When adding or removing users, you must also provide the ID for the role to give them (when adding) or the ID of the role they currently have (when removing). -There is no way to update a user's role directly at the moment. Instead, if you want to change a user's role, first remove the user from the project and then add them back with the desired role. - ### Add a user to a project ``` http @@ -704,6 +702,55 @@ http://localhost:4242/api/admin/projects/MyCoolProject/users/42/roles/13 \ Authorization:$KEY ``` +### Change a user's role in a project + +``` http +PUT /api/admin/projects/:projectId/users/:userId/roles/:roleId +``` + +This will change the user's project role to the role specified by `:roleId`. If the user has not been added to the project, nothing happens. + +#### URL parameters + +| Parameter | Type | Description | Example value | +|-------------|---------|------------------------------------------------------|-------------------| +| `userId` | integer | The ID of the user whose role you want to update. | `1` | +| `projectId` | string | The id of the relevant project. | `"MyCoolProject"` | +| `roleId` | integer | The role ID of the role you wish to assign the user. | `7` | + + +#### Responses + +
+Responses data + +##### 200 OK + +The user's role has been successfully changed. This response has no body. + +##### 400 Bad Request + +You tried to change the role of only user with the `owner` role in the project: + +``` json +[ + { + "msg": "A project must have at least one owner." + } +] +``` + +
+ +#### Example query + +The following query would change the role of the user with ID 42 the role with ID 13 in the _MyCoolProject_ project. +```bash +http PUT \ +http://localhost:4242/api/admin/projects/MyCoolProject/users/42/roles/13 \ +Authorization:$KEY +``` + ### Remove a user from a project ``` http @@ -720,6 +767,50 @@ This will remove the specified user from the project. The user _must_ have the r | `projectId` | string | The id of the project to remove the user from. | `"MyCoolProject"` | | `roleId` | integer | The current role the of the user you want to remove from the project. | `7` | + +#### Responses + +
+Responses data + +##### 200 OK + +The user has been removed from the project. This response has no body. + +##### 400 Bad Request + +No user with the current role exists in the project: + + +``` json +[ + { + "msg": "Couldn't find roleId= on project=" + } +] +``` + +You tried to remove the only user with the role `owner` in the project: + +``` json +[ + { + "msg": "A project must have at least one owner." + } +] +``` + +
+ +#### Example query + +The following query would remove the user with ID 42 and role ID 13 from the _MyCoolProject_ project. +```bash +http DELETE \ +http://localhost:4242/api/admin/projects/MyCoolProject/users/42/roles/13 \ +Authorization:$KEY +``` + #### Responses
From 38ed3816a89d08c13e4aa923705b51ec2952afc2 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Thu, 3 Mar 2022 14:17:43 +0100 Subject: [PATCH 45/67] docs: Remove doubled section and clarify role removal --- .../docs/api/admin/feature-toggles-api-v2.md | 51 +------------------ 1 file changed, 2 insertions(+), 49 deletions(-) diff --git a/website/docs/api/admin/feature-toggles-api-v2.md b/website/docs/api/admin/feature-toggles-api-v2.md index a4b4d5c325..78e22b43cb 100644 --- a/website/docs/api/admin/feature-toggles-api-v2.md +++ b/website/docs/api/admin/feature-toggles-api-v2.md @@ -757,7 +757,7 @@ Authorization:$KEY DELETE /api/admin/projects/:projectId/users/:userId/roles/:roleId ``` -This will remove the specified user from the project. The user _must_ have the role indicated by the `:roleId` URL parameter for the request to succeed. +This removes the specified role from the user in the project. Because users can only have one role in a project, this effectively removes the user from the project. The user _must_ have the role indicated by the `:roleId` URL parameter for the request to succeed. #### URL parameters @@ -775,54 +775,7 @@ This will remove the specified user from the project. The user _must_ have the r ##### 200 OK -The user has been removed from the project. This response has no body. - -##### 400 Bad Request - -No user with the current role exists in the project: - - -``` json -[ - { - "msg": "Couldn't find roleId= on project=" - } -] -``` - -You tried to remove the only user with the role `owner` in the project: - -``` json -[ - { - "msg": "A project must have at least one owner." - } -] -``` - -
- -#### Example query - -The following query would remove the user with ID 42 and role ID 13 from the _MyCoolProject_ project. -```bash -http DELETE \ -http://localhost:4242/api/admin/projects/MyCoolProject/users/42/roles/13 \ -Authorization:$KEY -``` - -#### Responses - -
-Responses data - -:::caution -If you provide the wrong role id for the user, but there is another user in the same project with the role you provided, you'll get a 200 OK response indicating success, but the user will not be removed. -::: - -##### 200 OK - -The user has been removed from the project. This response has no body. +The no longer has the specified role in the project. If the user had this role prior to this API request, they will have been removed from the project. This response has no body. ##### 400 Bad Request From d4521a1c0cc721f0911cad8aa9b8c51803561914 Mon Sep 17 00:00:00 2001 From: Youssef Date: Thu, 3 Mar 2022 14:25:14 +0100 Subject: [PATCH 46/67] fix: changeRole to assign roles without existing members Co-authored-by: Christopher Kolstad --- src/lib/error/project-without-owner-error.ts | 23 ++++++++++++ src/lib/routes/util.ts | 2 + src/lib/services/project-service.ts | 6 +-- .../e2e/services/project-service.e2e.test.ts | 37 +++++++++++++++++++ 4 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 src/lib/error/project-without-owner-error.ts diff --git a/src/lib/error/project-without-owner-error.ts b/src/lib/error/project-without-owner-error.ts new file mode 100644 index 0000000000..ae9bb6fb09 --- /dev/null +++ b/src/lib/error/project-without-owner-error.ts @@ -0,0 +1,23 @@ +export default class ProjectWithoutOwnerError extends Error { + constructor() { + super(); + Error.captureStackTrace(this, this.constructor); + + this.name = this.constructor.name; + this.message = 'A project must have at least one owner'; + } + + toJSON(): any { + const obj = { + isJoi: true, + name: this.constructor.name, + details: [ + { + validationErrors: [], + message: this.message, + }, + ], + }; + return obj; + } +} diff --git a/src/lib/routes/util.ts b/src/lib/routes/util.ts index 07798cbd20..0b994fa5fa 100644 --- a/src/lib/routes/util.ts +++ b/src/lib/routes/util.ts @@ -66,6 +66,8 @@ export const handleErrors: ( return res.status(409).json(error).end(); case 'RoleInUseError': return res.status(400).json(error).end(); + case 'ProjectWithoutOwnerError': + return res.status(409).json(error).end(); default: logger.error('Server failed executing request', error); return res.status(500).end(); diff --git a/src/lib/services/project-service.ts b/src/lib/services/project-service.ts index 8281a0fb2e..5208fb04aa 100644 --- a/src/lib/services/project-service.ts +++ b/src/lib/services/project-service.ts @@ -36,6 +36,7 @@ import NoAccessError from '../error/no-access-error'; import IncompatibleProjectError from '../error/incompatible-project-error'; import { DEFAULT_PROJECT } from '../types/project'; import { IFeatureTagStore } from 'lib/types/stores/feature-tag-store'; +import ProjectWithoutOwnerError from '../error/project-without-owner-error'; const getCreatedBy = (user: User) => user.email || user.username; @@ -349,7 +350,7 @@ export default class ProjectService { projectId, ); if (users.length < 2) { - throw new Error('A project must have at least one owner'); + throw new ProjectWithoutOwnerError(); } } } @@ -360,8 +361,6 @@ export default class ProjectService { userId: number, createdBy: string, ): Promise { - const role = await this.findProjectRole(projectId, roleId); - const usersWithRoles = await this.getUsersWithAccess(projectId); const user = usersWithRoles.users.find((u) => u.id === userId); const currentRole = usersWithRoles.roles.find( @@ -380,6 +379,7 @@ export default class ProjectService { roleId, projectId, ); + const role = await this.findProjectRole(projectId, roleId); await this.eventStore.store( new ProjectUserUpdateRoleEvent({ diff --git a/src/test/e2e/services/project-service.e2e.test.ts b/src/test/e2e/services/project-service.e2e.test.ts index 2f72fc3e1b..9a13ccf49e 100644 --- a/src/test/e2e/services/project-service.e2e.test.ts +++ b/src/test/e2e/services/project-service.e2e.test.ts @@ -685,6 +685,43 @@ test('should update role for user on project', async () => { expect(ownerUsers).toHaveLength(2); }); +test('should able to assign role without existing members', async () => { + const project = { + id: 'update-users-test', + name: 'New project', + description: 'Blah', + }; + await projectService.createProject(project, user); + + const projectMember1 = await stores.userStore.insert({ + name: 'Some Member', + email: 'update1999@getunleash.io', + }); + + const testRole = await stores.roleStore.create({ + name: 'Power user', + roleType: 'custom', + description: 'Grants access to modify all environments', + }); + + const memberRole = await stores.roleStore.getRoleByName(RoleName.MEMBER); + + await projectService.addUser(project.id, memberRole.id, projectMember1.id); + await projectService.changeRole( + project.id, + testRole.id, + projectMember1.id, + 'test', + ); + + const { users } = await projectService.getUsersWithAccess(project.id, user); + const memberUsers = users.filter((user) => user.roleId === memberRole.id); + const testUsers = users.filter((user) => user.roleId === testRole.id); + + expect(memberUsers).toHaveLength(0); + expect(testUsers).toHaveLength(1); +}); + test('should not update role for user on project when she is the owner', async () => { const project = { id: 'update-users-not-allowed', From 4c4f6a3aaad0a73d5472944882d52d64d1280e11 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Thu, 3 Mar 2022 15:24:15 +0100 Subject: [PATCH 47/67] docs: update description of sendEmail option --- website/docs/api/admin/user-admin.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/website/docs/api/admin/user-admin.md b/website/docs/api/admin/user-admin.md index 067c39f075..9ea63af5a7 100644 --- a/website/docs/api/admin/user-admin.md +++ b/website/docs/api/admin/user-admin.md @@ -119,12 +119,12 @@ Creates a new user with the given root role. **Payload properties** -| Property name | Required | Description | Example value(s) | -|---------------|----------|---------------------------------------------------------------------------------|------------------------| -| `email` | Yes | The user's email address. | `"user@getunleash.io"` | -| `name` | Yes | The user's name | `"Some Name"` | -| `rootRole` | Yes | The role to assign to the user. Can be either the role's ID or its unique name. | `2`, `"Editor"` | -| `sendEmail` | No | Whether to send a registration email to the user or not. Defaults to `true`. | `false` | +| Property name | Required | Description | Example value(s) | +|---------------|----------|-------------------------------------------------------------------------------------------|------------------------| +| `email` | Yes | The user's email address. | `"user@getunleash.io"` | +| `name` | Yes | The user's name | `"Some Name"` | +| `rootRole` | Yes | The role to assign to the user. Can be either the role's ID or its unique name. | `2`, `"Editor"` | +| `sendEmail` | No | Whether to send a welcome email with a login link to the user or not. Defaults to `true`. | `false` | **Body** From 49af49a9d4bc9807eb5c87fef0c9afe20e8a411a Mon Sep 17 00:00:00 2001 From: Youssef Date: Thu, 3 Mar 2022 16:26:25 +0100 Subject: [PATCH 48/67] fix: lint --- src/test/e2e/services/project-service.e2e.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/e2e/services/project-service.e2e.test.ts b/src/test/e2e/services/project-service.e2e.test.ts index 9a13ccf49e..ee3faf6f48 100644 --- a/src/test/e2e/services/project-service.e2e.test.ts +++ b/src/test/e2e/services/project-service.e2e.test.ts @@ -715,8 +715,8 @@ test('should able to assign role without existing members', async () => { ); const { users } = await projectService.getUsersWithAccess(project.id, user); - const memberUsers = users.filter((user) => user.roleId === memberRole.id); - const testUsers = users.filter((user) => user.roleId === testRole.id); + const memberUsers = users.filter((u) => u.roleId === memberRole.id); + const testUsers = users.filter((u) => u.roleId === testRole.id); expect(memberUsers).toHaveLength(0); expect(testUsers).toHaveLength(1); From e6d939508d5d9b2f72c59d6fd94cdafcc44e42a1 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Fri, 4 Mar 2022 08:20:55 +0100 Subject: [PATCH 49/67] fix: correct the description of disabled variants --- website/docs/sdks/unleash-proxy.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/sdks/unleash-proxy.md b/website/docs/sdks/unleash-proxy.md index 7a7edc58ce..20ed110584 100644 --- a/website/docs/sdks/unleash-proxy.md +++ b/website/docs/sdks/unleash-proxy.md @@ -215,7 +215,7 @@ The data for a toggle without [variants](../advanced/feature-toggle-variants.md) - **`name`**: the name of the feature. - **`enabled`**: whether the toggle is enabled or not. Will always be `true`. -- **`variant`**: describes whether the toggle has variants and, if it does, what variant is active for this user. If a toggle doesn't have any variants, it will always be `{"name": "disabled", "enabled": true}`. +- **`variant`**: describes whether the toggle has variants and, if it does, what variant is active for this user. If a toggle doesn't have any variants, it will always be `{"name": "disabled", "enabled": false}`. :::note Unleash uses a fallback variant called "disabled" to indicate that a toggle has no variants. However, you are free to create a variant called "disabled" yourself. In that case you can tell them apart by checking the variant's `enabled` property: if the toggle has no variants, `enabled` will be `false`. If the toggle is the "disabled" variant that you created, it will have `enabled` set to `true`. From c9dd60e1ea3b8af34fe8b8ea967de07631da6112 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Fri, 4 Mar 2022 08:43:28 +0100 Subject: [PATCH 50/67] Apply suggestions from code review Co-authored-by: Christopher Kolstad --- website/docs/api/admin/feature-toggles-api-v2.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/docs/api/admin/feature-toggles-api-v2.md b/website/docs/api/admin/feature-toggles-api-v2.md index 78e22b43cb..9cd4b6272f 100644 --- a/website/docs/api/admin/feature-toggles-api-v2.md +++ b/website/docs/api/admin/feature-toggles-api-v2.md @@ -730,7 +730,7 @@ The user's role has been successfully changed. This response has no body. ##### 400 Bad Request -You tried to change the role of only user with the `owner` role in the project: +You tried to change the role of the only user with the `owner` role in the project: ``` json [ @@ -775,7 +775,7 @@ This removes the specified role from the user in the project. Because users can ##### 200 OK -The no longer has the specified role in the project. If the user had this role prior to this API request, they will have been removed from the project. This response has no body. +The user no longer has the specified role in the project. If the user had this role prior to this API request, they will have been removed from the project. This response has no body. ##### 400 Bad Request From 6d433c50ab66b9d539da8e31f417b126053aac37 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Fri, 4 Mar 2022 10:36:40 +0100 Subject: [PATCH 51/67] docs: add info about requiring `name` OR `email` --- website/docs/api/admin/user-admin.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/website/docs/api/admin/user-admin.md b/website/docs/api/admin/user-admin.md index 9ea63af5a7..e410b966f0 100644 --- a/website/docs/api/admin/user-admin.md +++ b/website/docs/api/admin/user-admin.md @@ -119,10 +119,14 @@ Creates a new user with the given root role. **Payload properties** +:::info Requirements +The payload **must** contain **at least one of** the `name` and `email` properties, though which one is up to you. For the user to be able to log in to the system, the user **must** have an email. +::: + | Property name | Required | Description | Example value(s) | |---------------|----------|-------------------------------------------------------------------------------------------|------------------------| -| `email` | Yes | The user's email address. | `"user@getunleash.io"` | -| `name` | Yes | The user's name | `"Some Name"` | +| `email` | No | The user's email address. Must be provided if `name` is not provided. | `"user@getunleash.io"` | +| `name` | No | The user's name. Must be provided if `email` is not provided. | `"Some Name"` | | `rootRole` | Yes | The role to assign to the user. Can be either the role's ID or its unique name. | `2`, `"Editor"` | | `sendEmail` | No | Whether to send a welcome email with a login link to the user or not. Defaults to `true`. | `false` | From 4705fe22cc574afc3476cbd32df83a9ef6073d21 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Fri, 4 Mar 2022 10:41:24 +0100 Subject: [PATCH 52/67] docs: remove error payload that no longer applies. --- website/docs/api/admin/feature-toggles-api-v2.md | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/website/docs/api/admin/feature-toggles-api-v2.md b/website/docs/api/admin/feature-toggles-api-v2.md index 9cd4b6272f..669b093cf9 100644 --- a/website/docs/api/admin/feature-toggles-api-v2.md +++ b/website/docs/api/admin/feature-toggles-api-v2.md @@ -779,17 +779,6 @@ The user no longer has the specified role in the project. If the user had this r ##### 400 Bad Request -No user with the current role exists in the project: - - -``` json -[ - { - "msg": "Couldn't find roleId= on project=" - } -] -``` - You tried to remove the only user with the role `owner` in the project: ``` json From 28b16d922d8491a350598f134ba4642a4e684386 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Fri, 4 Mar 2022 12:41:13 +0100 Subject: [PATCH 53/67] docs: reword and add additional deets re: activation strategy impl --- website/docs/user_guide/activation-strategies.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/website/docs/user_guide/activation-strategies.md b/website/docs/user_guide/activation-strategies.md index 0fd6d7bdfd..9609fa192e 100644 --- a/website/docs/user_guide/activation-strategies.md +++ b/website/docs/user_guide/activation-strategies.md @@ -5,7 +5,11 @@ title: Activation Strategies It is powerful to be able to turn a feature on and off instantaneously, without redeploying the application. The next level of control comes when you are able to enable a feature for specific users or enable it for a small subset of users. We achieve this level of control with the help of activation strategies. The most straightforward strategy is the standard strategy, which basically means that the feature should be enabled to everyone. -The definition of an activation strategy lives in the Unleash API and can be created via the Unleash UI. The implementation of activation strategies lives in various client implementations. +Unleash comes with a number of built-in strategies (described below) and also lets you add your own [custom activation strategies](../custom-activation-strategy.md) if you need more control. +However, while activation strategies are *defined* on the server, the server does not *implement* the strategies. Instead, activation strategy *implementation* (and thus feature toggle *evaluation*) is done client-side. +Thus, all [server-side client SDKs](../sdks/index.md#server-side-sdks) and the [Unleash Proxy](../sdks/unleash-proxy.md) implement the default strategies +(and allow you to add your own custom strategy implementations). +The [front-end client SDKs](../sdks/index.md#front-end-sdks) do not do the evaluation themselves: instead relying on the [Unleash Proxy](../sdks/unleash-proxy.md) to take care of the implementation and evaluation. Unleash comes with a few common activation strategies. Some of them require the client to provide the [unleash-context](unleash-context.md), which gives the necessary context for Unleash. The built-in activation strategies are: From 9b691a4f892f9ffff27dec50f4c2600dbbe6b075 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Fri, 4 Mar 2022 12:43:58 +0100 Subject: [PATCH 54/67] fix: fix broken link custom activation strategies doc. --- website/docs/user_guide/activation-strategies.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/user_guide/activation-strategies.md b/website/docs/user_guide/activation-strategies.md index 9609fa192e..462749aa31 100644 --- a/website/docs/user_guide/activation-strategies.md +++ b/website/docs/user_guide/activation-strategies.md @@ -5,7 +5,7 @@ title: Activation Strategies It is powerful to be able to turn a feature on and off instantaneously, without redeploying the application. The next level of control comes when you are able to enable a feature for specific users or enable it for a small subset of users. We achieve this level of control with the help of activation strategies. The most straightforward strategy is the standard strategy, which basically means that the feature should be enabled to everyone. -Unleash comes with a number of built-in strategies (described below) and also lets you add your own [custom activation strategies](../custom-activation-strategy.md) if you need more control. +Unleash comes with a number of built-in strategies (described below) and also lets you add your own [custom activation strategies](../advanced/custom-activation-strategy.md) if you need more control. However, while activation strategies are *defined* on the server, the server does not *implement* the strategies. Instead, activation strategy *implementation* (and thus feature toggle *evaluation*) is done client-side. Thus, all [server-side client SDKs](../sdks/index.md#server-side-sdks) and the [Unleash Proxy](../sdks/unleash-proxy.md) implement the default strategies (and allow you to add your own custom strategy implementations). From 28df5dcb311a72e8ebf8567afba3d368cba89a71 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Fri, 4 Mar 2022 12:56:16 +0100 Subject: [PATCH 55/67] docs: add offline headings / rough skeleton --- website/docs/sdks/index.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/website/docs/sdks/index.md b/website/docs/sdks/index.md index bebee77431..0142471309 100644 --- a/website/docs/sdks/index.md +++ b/website/docs/sdks/index.md @@ -117,9 +117,13 @@ Here's some of the fantastic work our community has done to make Unleash work in - [uekoetter.dev/unleash-client-dart](https://pub.dev/packages/unleash) (Dart) - _...your implementation for your favorite language._ -## Implement your own SDK {#implement-your-own-sdk} +### Implement your own SDK {#implement-your-own-sdk} If you can't find an SDK that fits your need, you can also develop your own SDK. To make implementation easier, check out these resources: - [Unleash Client Specifications](https://github.com/Unleash/client-specification) - Used by all official SDKs to make sure they behave correctly across different language implementations. This lets us verify that a gradual rollout to 10% of the users would affect the same users regardless of which SDK you're using. - [Client SDK overview](../client-specification) - A brief, overall guide of the _Unleash Architecture_ and important aspects of the SDK role in it all. + +## How the SDKs work + +### Working offline From cda31e46d3a94896acadef41273b7a6b0f34539d Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Fri, 4 Mar 2022 12:56:55 +0100 Subject: [PATCH 56/67] fix: remove unintended line break. --- website/docs/user_guide/activation-strategies.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/website/docs/user_guide/activation-strategies.md b/website/docs/user_guide/activation-strategies.md index 462749aa31..fb6385b884 100644 --- a/website/docs/user_guide/activation-strategies.md +++ b/website/docs/user_guide/activation-strategies.md @@ -7,8 +7,7 @@ It is powerful to be able to turn a feature on and off instantaneously, without Unleash comes with a number of built-in strategies (described below) and also lets you add your own [custom activation strategies](../advanced/custom-activation-strategy.md) if you need more control. However, while activation strategies are *defined* on the server, the server does not *implement* the strategies. Instead, activation strategy *implementation* (and thus feature toggle *evaluation*) is done client-side. -Thus, all [server-side client SDKs](../sdks/index.md#server-side-sdks) and the [Unleash Proxy](../sdks/unleash-proxy.md) implement the default strategies -(and allow you to add your own custom strategy implementations). +Thus, all [server-side client SDKs](../sdks/index.md#server-side-sdks) and the [Unleash Proxy](../sdks/unleash-proxy.md) implement the default strategies (and allow you to add your own custom strategy implementations). The [front-end client SDKs](../sdks/index.md#front-end-sdks) do not do the evaluation themselves: instead relying on the [Unleash Proxy](../sdks/unleash-proxy.md) to take care of the implementation and evaluation. Unleash comes with a few common activation strategies. Some of them require the client to provide the [unleash-context](unleash-context.md), which gives the necessary context for Unleash. The built-in activation strategies are: From f35a932e8db4a34a62d9be578ad31482236dcc8a Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Fri, 4 Mar 2022 13:02:06 +0100 Subject: [PATCH 57/67] fix: stitch the info paragraph together with the existing content --- website/docs/user_guide/activation-strategies.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/website/docs/user_guide/activation-strategies.md b/website/docs/user_guide/activation-strategies.md index fb6385b884..123e80e369 100644 --- a/website/docs/user_guide/activation-strategies.md +++ b/website/docs/user_guide/activation-strategies.md @@ -10,7 +10,9 @@ However, while activation strategies are *defined* on the server, the server doe Thus, all [server-side client SDKs](../sdks/index.md#server-side-sdks) and the [Unleash Proxy](../sdks/unleash-proxy.md) implement the default strategies (and allow you to add your own custom strategy implementations). The [front-end client SDKs](../sdks/index.md#front-end-sdks) do not do the evaluation themselves: instead relying on the [Unleash Proxy](../sdks/unleash-proxy.md) to take care of the implementation and evaluation. -Unleash comes with a few common activation strategies. Some of them require the client to provide the [unleash-context](unleash-context.md), which gives the necessary context for Unleash. The built-in activation strategies are: +Some activation strategies require the client to provide the current [Unleash context](unleash-context.md) to the toggle evaluation function for the evaluation to be done correctly. + +The following activation strategies are bundled with Unleash and always available: - [Standard](#standard) - [UserIDs](#userids) From 6a4e2b716375fddec560c09bc0b5e0cab36d0e7f Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Fri, 4 Mar 2022 13:04:40 +0100 Subject: [PATCH 58/67] fix: change : to , --- website/docs/user_guide/activation-strategies.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/user_guide/activation-strategies.md b/website/docs/user_guide/activation-strategies.md index 123e80e369..7989ce3662 100644 --- a/website/docs/user_guide/activation-strategies.md +++ b/website/docs/user_guide/activation-strategies.md @@ -8,7 +8,7 @@ It is powerful to be able to turn a feature on and off instantaneously, without Unleash comes with a number of built-in strategies (described below) and also lets you add your own [custom activation strategies](../advanced/custom-activation-strategy.md) if you need more control. However, while activation strategies are *defined* on the server, the server does not *implement* the strategies. Instead, activation strategy *implementation* (and thus feature toggle *evaluation*) is done client-side. Thus, all [server-side client SDKs](../sdks/index.md#server-side-sdks) and the [Unleash Proxy](../sdks/unleash-proxy.md) implement the default strategies (and allow you to add your own custom strategy implementations). -The [front-end client SDKs](../sdks/index.md#front-end-sdks) do not do the evaluation themselves: instead relying on the [Unleash Proxy](../sdks/unleash-proxy.md) to take care of the implementation and evaluation. +The [front-end client SDKs](../sdks/index.md#front-end-sdks) do not do the evaluation themselves, instead relying on the [Unleash Proxy](../sdks/unleash-proxy.md) to take care of the implementation and evaluation. Some activation strategies require the client to provide the current [Unleash context](unleash-context.md) to the toggle evaluation function for the evaluation to be done correctly. From 304e95f9d7d5ce30c97276130c03ee60a1f99399 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Fri, 4 Mar 2022 13:37:59 +0100 Subject: [PATCH 59/67] docs: add working offline section to the SDKs doc --- website/docs/sdks/index.md | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/website/docs/sdks/index.md b/website/docs/sdks/index.md index 0142471309..feb8208a56 100644 --- a/website/docs/sdks/index.md +++ b/website/docs/sdks/index.md @@ -124,6 +124,17 @@ If you can't find an SDK that fits your need, you can also develop your own SDK. - [Unleash Client Specifications](https://github.com/Unleash/client-specification) - Used by all official SDKs to make sure they behave correctly across different language implementations. This lets us verify that a gradual rollout to 10% of the users would affect the same users regardless of which SDK you're using. - [Client SDK overview](../client-specification) - A brief, overall guide of the _Unleash Architecture_ and important aspects of the SDK role in it all. -## How the SDKs work +## Working offline -### Working offline +Once they have been initialised, all Unleash clients will continue to work perfectly well without an internet connection or in the event that the Unleash Server has an outage. + +Because the SDKs and the Unleash Proxy cache their feature toggle states locally and only communicate with the Unleash server (in the case of the server-side SDKs and the Proxy) or the Proxy (in the case of front-end SDKs) at predetermined intervals, a broken connection only means that they won't get any new updates. + +### Bootstrapping + +By default, all SDKs reach out to the Unleash Server at startup to fetch their toggle configuration. Additionally some of the server-side SDKs and the Proxy (see the above [compatibility table](#server-side-sdk-compatibility-table)) also support *bootstrapping*, which allows them to get their toggle configuration from a file, the environment, or other local resources. These SDKs can work without any network connection whatsoever. + +Bootstrapping is also supported by the following front-end client SDKs: +- [the JavaScript proxy client](/sdks/proxy-javascript) +- [the React Proxy client](/sdks/proxy-react) +- [the Android proxy client](/sdks/proxy-react) From c0985ad08dfc789f6263c62e2c5728a01b61e676 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Fri, 4 Mar 2022 13:43:02 +0100 Subject: [PATCH 60/67] docs: be explicit about what happens if an SDK doesn't have toggles. --- website/docs/sdks/index.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/website/docs/sdks/index.md b/website/docs/sdks/index.md index feb8208a56..fb7840fb75 100644 --- a/website/docs/sdks/index.md +++ b/website/docs/sdks/index.md @@ -130,6 +130,8 @@ Once they have been initialised, all Unleash clients will continue to work perfe Because the SDKs and the Unleash Proxy cache their feature toggle states locally and only communicate with the Unleash server (in the case of the server-side SDKs and the Proxy) or the Proxy (in the case of front-end SDKs) at predetermined intervals, a broken connection only means that they won't get any new updates. +Unless the SDK supports [bootstrapping](#bootstrapping) it *will* need to connect to Unleash at startup to get its initial feature toggle data set. If the SDK doesn't have a feature toggle data set available, all toggles will fall back to evaluating as disabled or as the specified default value (in SDKs that support that). + ### Bootstrapping By default, all SDKs reach out to the Unleash Server at startup to fetch their toggle configuration. Additionally some of the server-side SDKs and the Proxy (see the above [compatibility table](#server-side-sdk-compatibility-table)) also support *bootstrapping*, which allows them to get their toggle configuration from a file, the environment, or other local resources. These SDKs can work without any network connection whatsoever. From 300c6abd74abb552eac92a62fbdaf2e792875fb3 Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Fri, 4 Mar 2022 13:52:19 +0100 Subject: [PATCH 61/67] fix: fix broken link in SDK list --- website/docs/sdks/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/docs/sdks/index.md b/website/docs/sdks/index.md index fb7840fb75..dd04111233 100644 --- a/website/docs/sdks/index.md +++ b/website/docs/sdks/index.md @@ -139,4 +139,4 @@ By default, all SDKs reach out to the Unleash Server at startup to fetch their t Bootstrapping is also supported by the following front-end client SDKs: - [the JavaScript proxy client](/sdks/proxy-javascript) - [the React Proxy client](/sdks/proxy-react) -- [the Android proxy client](/sdks/proxy-react) +- [the Android proxy client](/sdks/android_proxy_sdk) From 6f075e4d1ca15070bd1e27f444fe7c2892870d08 Mon Sep 17 00:00:00 2001 From: Fredrik Strand Oseberg Date: Fri, 4 Mar 2022 17:29:42 +0100 Subject: [PATCH 62/67] Feat/new constraint operators (#1397) * feat: add migration for currentTime context field * feat: add tests for number validator * feat: add validation fields for constraint * feat: add validation for semver, date and legalvalues * fix: import paths * fix: only allow specified operators * fix: add operator test * fix: reset db * fix: remove unused import * fix: set semver as dependency --- package.json | 3 +- src/lib/routes/admin-api/project/features.ts | 15 ++- src/lib/schema/constraint-value-types.test.ts | 77 +++++++++++++++ src/lib/schema/constraint-value-types.ts | 7 ++ src/lib/schema/feature-schema.test.ts | 19 +++- src/lib/schema/feature-schema.ts | 6 +- src/lib/services/feature-toggle-service.ts | 74 +++++++++++++- src/lib/types/model.ts | 5 +- src/lib/util/constants.ts | 47 +++++++++ .../util/validators/constraint-types.test.ts | 97 +++++++++++++++++++ src/lib/util/validators/constraint-types.ts | 49 ++++++++++ ...24111626-add-current-time-context-field.js | 19 ++++ 12 files changed, 412 insertions(+), 6 deletions(-) create mode 100644 src/lib/schema/constraint-value-types.test.ts create mode 100644 src/lib/schema/constraint-value-types.ts create mode 100644 src/lib/util/validators/constraint-types.test.ts create mode 100644 src/lib/util/validators/constraint-types.ts create mode 100644 src/migrations/20220224111626-add-current-time-context-field.js diff --git a/package.json b/package.json index 38d6cc334a..c7a81eb252 100644 --- a/package.json +++ b/package.json @@ -113,7 +113,8 @@ "stoppable": "^1.1.0", "type-is": "^1.6.18", "unleash-frontend": "4.8.0", - "uuid": "^8.3.2" + "uuid": "^8.3.2", + "semver": "^7.3.5" }, "devDependencies": { "@babel/core": "7.17.5", diff --git a/src/lib/routes/admin-api/project/features.ts b/src/lib/routes/admin-api/project/features.ts index a677a1c01e..14f0390b7c 100644 --- a/src/lib/routes/admin-api/project/features.ts +++ b/src/lib/routes/admin-api/project/features.ts @@ -10,6 +10,7 @@ import { CREATE_FEATURE_STRATEGY, DELETE_FEATURE, DELETE_FEATURE_STRATEGY, + NONE, UPDATE_FEATURE, UPDATE_FEATURE_ENVIRONMENT, UPDATE_FEATURE_STRATEGY, @@ -84,7 +85,6 @@ export default class ProjectFeaturesController extends Controller { this.toggleEnvironmentOff, UPDATE_FEATURE_ENVIRONMENT, ); - // activation strategies this.get(`${PATH_STRATEGIES}`, this.getStrategies); this.post( @@ -92,6 +92,7 @@ export default class ProjectFeaturesController extends Controller { this.addStrategy, CREATE_FEATURE_STRATEGY, ); + this.get(`${PATH_STRATEGY}`, this.getStrategy); this.put( `${PATH_STRATEGY}`, @@ -108,6 +109,11 @@ export default class ProjectFeaturesController extends Controller { this.deleteStrategy, DELETE_FEATURE_STRATEGY, ); + this.post( + `${PATH_FEATURE}/constraint/validate`, + this.validateConstraint, + NONE, + ); // feature toggles this.get(PATH, this.getFeatures); @@ -337,6 +343,13 @@ export default class ProjectFeaturesController extends Controller { res.status(200).json(updatedStrategy); } + async validateConstraint(req: Request, res: Response): Promise { + const constraint: IConstraint = { ...req.body }; + + await this.featureService.validateConstraint(constraint); + res.status(204).send(); + } + async getStrategy( req: IAuthRequest, res: Response, diff --git a/src/lib/schema/constraint-value-types.test.ts b/src/lib/schema/constraint-value-types.test.ts new file mode 100644 index 0000000000..906c25f698 --- /dev/null +++ b/src/lib/schema/constraint-value-types.test.ts @@ -0,0 +1,77 @@ +import { + constraintDateTypeSchema, + constraintNumberTypeSchema, + constraintStringTypeSchema, +} from './constraint-value-types'; + +/* Number type */ +test('should require number', async () => { + try { + await constraintNumberTypeSchema.validateAsync('test'); + } catch (error) { + expect(error.details[0].message).toEqual('"value" must be a number'); + } +}); + +test('should allow strings that can be parsed to a number', async () => { + await constraintNumberTypeSchema.validateAsync('5'); +}); + +test('should allow floating point numbers', async () => { + await constraintNumberTypeSchema.validateAsync(5.72); +}); + +test('should allow numbers', async () => { + await constraintNumberTypeSchema.validateAsync(5); +}); + +test('should allow negative numbers', async () => { + await constraintNumberTypeSchema.validateAsync(-5); +}); + +/* String types */ +test('should require a list of strings', async () => { + expect.assertions(1); + try { + await constraintStringTypeSchema.validateAsync(['test', 1]); + } catch (error) { + expect(error.details[0].message).toEqual('"[1]" must be a string'); + } +}); + +test('should succeed with a list of strings', async () => { + expect.assertions(0); + await constraintStringTypeSchema.validateAsync([ + 'test', + 'another-test', + 'supervalue', + ]); +}); + +/* Date type */ + +test('should fail an invalid date', async () => { + expect.assertions(1); + + const invalidDate = 'Tuesday the awesome day'; + try { + await constraintDateTypeSchema.validateAsync(invalidDate); + } catch (error) { + expect(error.details[0].message).toEqual( + '"value" must be a valid date', + ); + } +}); + +test('Should pass a valid date', async () => { + expect.assertions(0); + + const invalidDate = '2022-01-29T13:00:00.000Z'; + try { + await constraintDateTypeSchema.validateAsync(invalidDate); + } catch (error) { + expect(error.details[0].message).toEqual( + '"value" must be a valid date', + ); + } +}); diff --git a/src/lib/schema/constraint-value-types.ts b/src/lib/schema/constraint-value-types.ts new file mode 100644 index 0000000000..5861395169 --- /dev/null +++ b/src/lib/schema/constraint-value-types.ts @@ -0,0 +1,7 @@ +import joi from 'joi'; + +export const constraintNumberTypeSchema = joi.number(); + +export const constraintStringTypeSchema = joi.array().items(joi.string()); + +export const constraintDateTypeSchema = joi.date(); diff --git a/src/lib/schema/feature-schema.test.ts b/src/lib/schema/feature-schema.test.ts index 236fdb047e..700eb5bbc5 100644 --- a/src/lib/schema/feature-schema.test.ts +++ b/src/lib/schema/feature-schema.test.ts @@ -1,4 +1,4 @@ -import { featureSchema, querySchema } from './feature-schema'; +import { constraintSchema, featureSchema, querySchema } from './feature-schema'; test('should require URL firendly name', () => { const toggle = { @@ -272,3 +272,20 @@ test('Filter queries should reject project names that are not alphanum', () => { '"project[0]" must be URL friendly', ); }); + +test('constraint schema should only allow specified operators', async () => { + const invalidConstraint = { + contextName: 'semver', + operator: 'INVALID_OPERATOR', + value: 123123213123, + }; + expect.assertions(1); + + try { + await constraintSchema.validateAsync(invalidConstraint); + } catch (error) { + expect(error.message).toBe( + '"operator" must be one of [NOT_IN, IN, STR_ENDS_WITH, STR_STARTS_WITH, STR_CONTAINS, NUM_EQ, NUM_GT, NUM_GTE, NUM_LT, NUM_LTE, DATE_AFTER, DATE_BEFORE, SEMVER_EQ, SEMVER_GT, SEMVER_LT]', + ); + } +}); diff --git a/src/lib/schema/feature-schema.ts b/src/lib/schema/feature-schema.ts index 395d2c8e97..93bd22f152 100644 --- a/src/lib/schema/feature-schema.ts +++ b/src/lib/schema/feature-schema.ts @@ -1,4 +1,5 @@ import joi from 'joi'; +import { ALL_OPERATORS } from '../util/constants'; import { nameType } from '../routes/util'; export const nameSchema = joi @@ -8,8 +9,11 @@ export const nameSchema = joi export const constraintSchema = joi.object().keys({ contextName: joi.string(), - operator: joi.string(), + operator: joi.string().valid(...ALL_OPERATORS), values: joi.array().items(joi.string().min(1).max(100)).min(1).optional(), + value: joi.optional(), + caseInsensitive: joi.boolean().optional(), + inverted: joi.boolean().optional(), }); export const strategiesSchema = joi.object().keys({ diff --git a/src/lib/services/feature-toggle-service.ts b/src/lib/services/feature-toggle-service.ts index d237c87c62..3445a7629a 100644 --- a/src/lib/services/feature-toggle-service.ts +++ b/src/lib/services/feature-toggle-service.ts @@ -6,6 +6,7 @@ import NameExistsError from '../error/name-exists-error'; import InvalidOperationError from '../error/invalid-operation-error'; import { FOREIGN_KEY_VIOLATION } from '../error/db-error'; import { + constraintSchema, featureMetadataSchema, nameSchema, variantsArraySchema, @@ -39,6 +40,7 @@ import { FeatureToggleDTO, FeatureToggleLegacy, FeatureToggleWithEnvironment, + IConstraint, IEnvironmentDetail, IFeatureEnvironmentInfo, IFeatureOverview, @@ -50,9 +52,23 @@ import { } from '../types/model'; import { IFeatureEnvironmentStore } from '../types/stores/feature-environment-store'; import { IFeatureToggleClientStore } from '../types/stores/feature-toggle-client-store'; -import { DEFAULT_ENV } from '../util/constants'; +import { + DATE_OPERATORS, + DEFAULT_ENV, + NUM_OPERATORS, + SEMVER_OPERATORS, + STRING_OPERATORS, +} from '../util/constants'; import { applyPatch, deepClone, Operation } from 'fast-json-patch'; import { OperationDeniedError } from '../error/operation-denied-error'; +import { + validateDate, + validateLegalValues, + validateNumber, + validateSemver, + validateString, +} from '../util/validators/constraint-types'; +import { IContextFieldStore } from 'lib/types/stores/context-field-store'; interface IFeatureContext { featureName: string; @@ -63,6 +79,10 @@ interface IFeatureStrategyContext extends IFeatureContext { environment: string; } +const oneOf = (values: string[], match: string) => { + return values.some((value) => value === match); +}; + class FeatureToggleService { private logger: Logger; @@ -80,6 +100,8 @@ class FeatureToggleService { private eventStore: IEventStore; + private contextFieldStore: IContextFieldStore; + constructor( { featureStrategiesStore, @@ -89,6 +111,7 @@ class FeatureToggleService { eventStore, featureTagStore, featureEnvironmentStore, + contextFieldStore, }: Pick< IUnleashStores, | 'featureStrategiesStore' @@ -98,6 +121,7 @@ class FeatureToggleService { | 'eventStore' | 'featureTagStore' | 'featureEnvironmentStore' + | 'contextFieldStore' >, { getLogger }: Pick, ) { @@ -109,6 +133,7 @@ class FeatureToggleService { this.projectStore = projectStore; this.eventStore = eventStore; this.featureEnvironmentStore = featureEnvironmentStore; + this.contextFieldStore = contextFieldStore; } async validateFeatureContext({ @@ -140,6 +165,53 @@ class FeatureToggleService { } } + async validateConstraint(constraint: IConstraint): Promise { + const { operator } = constraint; + await constraintSchema.validateAsync(constraint); + const contextDefinition = await this.contextFieldStore.get( + constraint.contextName, + ); + + if (oneOf(NUM_OPERATORS, operator)) { + await validateNumber(constraint.value); + } + + if (oneOf(STRING_OPERATORS, operator)) { + await validateString(constraint.values); + } + + if (oneOf(SEMVER_OPERATORS, operator)) { + // Semver library is not asynchronous, so we do not + // need to await here. + validateSemver(constraint.value); + } + + if (oneOf(DATE_OPERATORS, operator)) { + validateDate(constraint.value); + } + + if ( + oneOf( + [...DATE_OPERATORS, ...SEMVER_OPERATORS, ...NUM_OPERATORS], + operator, + ) + ) { + if (contextDefinition?.legalValues?.length > 0) { + validateLegalValues( + contextDefinition.legalValues, + constraint.value, + ); + } + } else { + if (contextDefinition?.legalValues?.length > 0) { + validateLegalValues( + contextDefinition.legalValues, + constraint.values, + ); + } + } + } + async patchFeature( project: string, featureName: string, diff --git a/src/lib/types/model.ts b/src/lib/types/model.ts index 57641040a1..301b0df1c0 100644 --- a/src/lib/types/model.ts +++ b/src/lib/types/model.ts @@ -6,7 +6,10 @@ import { IUser } from './user'; export interface IConstraint { contextName: string; operator: string; - values: string[]; + values?: string[]; + value?: string; + inverted?: boolean; + caseInsensitive?: boolean; } export enum WeightType { VARIABLE = 'variable', diff --git a/src/lib/util/constants.ts b/src/lib/util/constants.ts index 45b0ab23b8..6b95bc9e76 100644 --- a/src/lib/util/constants.ts +++ b/src/lib/util/constants.ts @@ -5,3 +5,50 @@ export const ENVIRONMENT_PERMISSION_TYPE = 'environment'; export const PROJECT_PERMISSION_TYPE = 'project'; export const CUSTOM_ROLE_TYPE = 'custom'; + +/* CONTEXT FIELD OPERATORS */ + +export const NOT_IN = 'NOT_IN'; +export const IN = 'IN'; +export const STR_ENDS_WITH = 'STR_ENDS_WITH'; +export const STR_STARTS_WITH = 'STR_STARTS_WITH'; +export const STR_CONTAINS = 'STR_CONTAINS'; +export const NUM_EQ = 'NUM_EQ'; +export const NUM_GT = 'NUM_GT'; +export const NUM_GTE = 'NUM_GTE'; +export const NUM_LT = 'NUM_LT'; +export const NUM_LTE = 'NUM_LTE'; +export const DATE_AFTER = 'DATE_AFTER'; +export const DATE_BEFORE = 'DATE_BEFORE'; +export const SEMVER_EQ = 'SEMVER_EQ'; +export const SEMVER_GT = 'SEMVER_GT'; +export const SEMVER_LT = 'SEMVER_LT'; + +export const ALL_OPERATORS = [ + NOT_IN, + IN, + STR_ENDS_WITH, + STR_STARTS_WITH, + STR_CONTAINS, + NUM_EQ, + NUM_GT, + NUM_GTE, + NUM_LT, + NUM_LTE, + DATE_AFTER, + DATE_BEFORE, + SEMVER_EQ, + SEMVER_GT, + SEMVER_LT, +]; + +export const STRING_OPERATORS = [ + STR_ENDS_WITH, + STR_STARTS_WITH, + STR_CONTAINS, + IN, + NOT_IN, +]; +export const NUM_OPERATORS = [NUM_EQ, NUM_GT, NUM_GTE, NUM_LT, NUM_LTE]; +export const DATE_OPERATORS = [DATE_AFTER, DATE_BEFORE]; +export const SEMVER_OPERATORS = [SEMVER_EQ, SEMVER_GT, SEMVER_LT]; diff --git a/src/lib/util/validators/constraint-types.test.ts b/src/lib/util/validators/constraint-types.test.ts new file mode 100644 index 0000000000..74945e27b2 --- /dev/null +++ b/src/lib/util/validators/constraint-types.test.ts @@ -0,0 +1,97 @@ +import { validateSemver, validateLegalValues } from './constraint-types'; + +test('semver validation should throw with bad format', () => { + const badSemver = 'a.b.c'; + expect.assertions(1); + + try { + validateSemver(badSemver); + } catch (e) { + expect(e.message).toBe( + `the provided value is not a valid semver format. The value provided was: ${badSemver}`, + ); + } +}); + +test('semver valdiation should pass with correct format', () => { + const validSemver = '1.2.3'; + expect.assertions(0); + + try { + validateSemver(validSemver); + } catch (e) { + expect(e.message).toBe( + `the provided value is not a valid semver format. The value provided was: ${validSemver}`, + ); + } +}); + +test('semver validation should fail partial semver', () => { + const partial = '1.2'; + expect.assertions(1); + + try { + validateSemver(partial); + } catch (e) { + expect(e.message).toBe( + `the provided value is not a valid semver format. The value provided was: ${partial}`, + ); + } +}); + +/* Legal values tests */ +test('should fail validation if value does not exist in single legal value', () => { + const legalValues = ['100', '200', '300']; + const value = '500'; + expect.assertions(1); + + try { + validateLegalValues(legalValues, value); + } catch (error) { + expect(error.message).toBe( + `${value} is not specified as a legal value on this context field`, + ); + } +}); + +test('should pass validation if value exists in single legal value', () => { + const legalValues = ['100', '200', '300']; + const value = '100'; + expect.assertions(0); + + try { + validateLegalValues(legalValues, value); + } catch (error) { + expect(error.message).toBe( + `${value} is not specified as a legal value on this context field`, + ); + } +}); + +test('should fail validation if one of the values does not exist in multiple legal values', () => { + const legalValues = ['100', '200', '300']; + const values = ['500', '100']; + expect.assertions(1); + + try { + validateLegalValues(legalValues, values); + } catch (error) { + expect(error.message).toBe( + `input values are not specified as a legal value on this context field`, + ); + } +}); + +test('should pass validation if all of the values exists in legal values', () => { + const legalValues = ['100', '200', '300']; + const values = ['200', '100']; + expect.assertions(0); + + try { + validateLegalValues(legalValues, values); + } catch (error) { + expect(error.message).toBe( + `input values are not specified as a legal value on this context field`, + ); + } +}); diff --git a/src/lib/util/validators/constraint-types.ts b/src/lib/util/validators/constraint-types.ts new file mode 100644 index 0000000000..6cbf56e704 --- /dev/null +++ b/src/lib/util/validators/constraint-types.ts @@ -0,0 +1,49 @@ +import semver from 'semver'; + +import { + constraintDateTypeSchema, + constraintNumberTypeSchema, + constraintStringTypeSchema, +} from '../../schema/constraint-value-types'; +import BadDataError from '../../error/bad-data-error'; + +export const validateNumber = async (value: unknown): Promise => { + await constraintNumberTypeSchema.validateAsync(value); +}; + +export const validateString = async (value: unknown): Promise => { + await constraintStringTypeSchema.validateAsync(value); +}; + +export const validateSemver = (value: unknown): void => { + const result = semver.valid(value); + + if (result) return; + throw new BadDataError( + `the provided value is not a valid semver format. The value provided was: ${value}`, + ); +}; + +export const validateDate = async (value: unknown): Promise => { + await constraintDateTypeSchema.validateAsync(value); +}; + +export const validateLegalValues = ( + legalValues: string[], + match: string[] | string, +): void => { + if (Array.isArray(match)) { + // Compare arrays to arrays + const valid = match.every((value) => legalValues.includes(value)); + if (!valid) + throw new BadDataError( + `input values are not specified as a legal value on this context field`, + ); + } else { + const valid = legalValues.includes(match); + if (!valid) + throw new BadDataError( + `${match} is not specified as a legal value on this context field`, + ); + } +}; diff --git a/src/migrations/20220224111626-add-current-time-context-field.js b/src/migrations/20220224111626-add-current-time-context-field.js new file mode 100644 index 0000000000..73aca8aae2 --- /dev/null +++ b/src/migrations/20220224111626-add-current-time-context-field.js @@ -0,0 +1,19 @@ +'use strict'; + +exports.up = function (db, cb) { + db.runSql( + ` + INSERT INTO context_fields(name, description, sort_order) VALUES('currentTime', 'Allows you to constrain on date values', 3); + `, + cb, + ); +}; + +exports.down = function (db, cb) { + db.runSql( + ` + DELETE FROM context_fields WHERE name = 'currentTime'; + `, + cb, + ); +}; From 0daf65d064bfa2c2cca9988fd5245e0a18a7f277 Mon Sep 17 00:00:00 2001 From: Fredrik Oseberg Date: Fri, 4 Mar 2022 17:35:02 +0100 Subject: [PATCH 63/67] chore: update frontend --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index c7a81eb252..1c5888473c 100644 --- a/package.json +++ b/package.json @@ -112,7 +112,7 @@ "serve-favicon": "^2.5.0", "stoppable": "^1.1.0", "type-is": "^1.6.18", - "unleash-frontend": "4.8.0", + "unleash-frontend": "4.9.0-beta.0", "uuid": "^8.3.2", "semver": "^7.3.5" }, diff --git a/yarn.lock b/yarn.lock index 9a48dbfdd3..b76ac438d2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7239,10 +7239,10 @@ universalify@^2.0.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== -unleash-frontend@4.8.0: - version "4.8.0" - resolved "https://registry.yarnpkg.com/unleash-frontend/-/unleash-frontend-4.8.0.tgz#c35961a740407fc0f4ab4ba24ea61f6bab02183b" - integrity sha512-GFT16muJ5ff9Xuz12OtZfsVC8ClTqEpoAP5tT0txNkUTauUSL1hTrvQsTeSJlY5THI7nKLrSBOGsKo0ysyiCXQ== +unleash-frontend@4.9.0-beta.0: + version "4.9.0-beta.0" + resolved "https://registry.yarnpkg.com/unleash-frontend/-/unleash-frontend-4.9.0-beta.0.tgz#80a6bbec605c3a19a6ac9e40f5e23f7b9648229c" + integrity sha512-ZR0jfUbw2MBI7Qr4yP1SbNiTwadNOPZ6D3Cu6W+HPsAtxOKrPWkDcdhMqBgXc5V+0OtFeppIV3JazBK5SrUHuA== unpipe@1.0.0, unpipe@~1.0.0: version "1.0.0" From 084752baf8ad78f52af26708b412e624af6912fe Mon Sep 17 00:00:00 2001 From: Fredrik Oseberg Date: Fri, 4 Mar 2022 17:35:23 +0100 Subject: [PATCH 64/67] 4.9.0-beta.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1c5888473c..fe8443d04a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "unleash-server", "description": "Unleash is an enterprise ready feature toggles service. It provides different strategies for handling feature toggles.", - "version": "4.8.2", + "version": "4.9.0-beta.0", "keywords": [ "unleash", "feature toggle", From e28aacccadafdb02a4f2e83f3854be0f3336bbdd Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sat, 5 Mar 2022 08:33:01 +0000 Subject: [PATCH 65/67] chore(deps): update dependency lint-staged to v12.3.5 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index fe8443d04a..2e9821a18d 100644 --- a/package.json +++ b/package.json @@ -150,7 +150,7 @@ "husky": "7.0.4", "jest": "27.5.1", "jest-fetch-mock": "3.0.3", - "lint-staged": "12.3.4", + "lint-staged": "12.3.5", "prettier": "2.5.1", "proxyquire": "2.1.3", "source-map-support": "0.5.21", diff --git a/yarn.lock b/yarn.lock index b76ac438d2..4d053c99f8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4866,10 +4866,10 @@ lines-and-columns@^1.1.6: resolved "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz" integrity sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA= -lint-staged@12.3.4: - version "12.3.4" - resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-12.3.4.tgz#4b1ff8c394c3e6da436aaec5afd4db18b5dac360" - integrity sha512-yv/iK4WwZ7/v0GtVkNb3R82pdL9M+ScpIbJLJNyCXkJ1FGaXvRCOg/SeL59SZtPpqZhE7BD6kPKFLIDUhDx2/w== +lint-staged@12.3.5: + version "12.3.5" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-12.3.5.tgz#8048ce048c3cac12f57200a06344a54dc91c8fa9" + integrity sha512-oOH36RUs1It7b9U/C7Nl/a0sLfoIBcMB8ramiB3nuJ6brBqzsWiUAFSR5DQ3yyP/OR7XKMpijtgKl2DV1lQ3lA== dependencies: cli-truncate "^3.1.0" colorette "^2.0.16" From 1f854a79ede1759f4b41a20770c7f0bdbb76fbd1 Mon Sep 17 00:00:00 2001 From: Renovate Bot Date: Sun, 6 Mar 2022 07:11:52 +0000 Subject: [PATCH 66/67] chore(deps): update dependency ts-node to v10.7.0 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 2e9821a18d..fe6c19f24e 100644 --- a/package.json +++ b/package.json @@ -157,7 +157,7 @@ "superagent": "7.1.1", "supertest": "6.2.2", "ts-jest": "27.1.3", - "ts-node": "10.6.0", + "ts-node": "10.7.0", "tsc-watch": "4.6.0", "typescript": "4.6.2" }, diff --git a/yarn.lock b/yarn.lock index 4d053c99f8..e6920e3e97 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7040,10 +7040,10 @@ ts-jest@27.1.3: semver "7.x" yargs-parser "20.x" -ts-node@10.6.0: - version "10.6.0" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.6.0.tgz#c3f4195d5173ce3affdc8f2fd2e9a7ac8de5376a" - integrity sha512-CJen6+dfOXolxudBQXnVjRVvYTmTWbyz7cn+xq2XTsvnaXbHqr4gXSCNbS2Jj8yTZMuGwUoBESLaOkLascVVvg== +ts-node@10.7.0: + version "10.7.0" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.7.0.tgz#35d503d0fab3e2baa672a0e94f4b40653c2463f5" + integrity sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A== dependencies: "@cspotcode/source-map-support" "0.7.0" "@tsconfig/node10" "^1.0.7" From c63c9b6fab4d06c1b53c59fe41ef8ffb5e25befb Mon Sep 17 00:00:00 2001 From: Thomas Heartman Date: Mon, 7 Mar 2022 11:11:51 +0100 Subject: [PATCH 67/67] docs: minor rewording and clarification around strategy impl/eval --- website/docs/user_guide/activation-strategies.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/website/docs/user_guide/activation-strategies.md b/website/docs/user_guide/activation-strategies.md index 7989ce3662..296b48aa06 100644 --- a/website/docs/user_guide/activation-strategies.md +++ b/website/docs/user_guide/activation-strategies.md @@ -6,8 +6,9 @@ title: Activation Strategies It is powerful to be able to turn a feature on and off instantaneously, without redeploying the application. The next level of control comes when you are able to enable a feature for specific users or enable it for a small subset of users. We achieve this level of control with the help of activation strategies. The most straightforward strategy is the standard strategy, which basically means that the feature should be enabled to everyone. Unleash comes with a number of built-in strategies (described below) and also lets you add your own [custom activation strategies](../advanced/custom-activation-strategy.md) if you need more control. -However, while activation strategies are *defined* on the server, the server does not *implement* the strategies. Instead, activation strategy *implementation* (and thus feature toggle *evaluation*) is done client-side. -Thus, all [server-side client SDKs](../sdks/index.md#server-side-sdks) and the [Unleash Proxy](../sdks/unleash-proxy.md) implement the default strategies (and allow you to add your own custom strategy implementations). +However, while activation strategies are *defined* on the server, the server does not *implement* the strategies. Instead, activation strategy implementation is done client-side. This means that it is *the client* that decides whether a feature should be enabled or not. + +All [server-side client SDKs](../sdks/index.md#server-side-sdks) and the [Unleash Proxy](../sdks/unleash-proxy.md) implement the default strategies (and allow you to add your own [custom strategy implementations](../advanced/custom-activation-strategy.md#implementation)). The [front-end client SDKs](../sdks/index.md#front-end-sdks) do not do the evaluation themselves, instead relying on the [Unleash Proxy](../sdks/unleash-proxy.md) to take care of the implementation and evaluation. Some activation strategies require the client to provide the current [Unleash context](unleash-context.md) to the toggle evaluation function for the evaluation to be done correctly.