1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-07-26 13:48:33 +02:00

Merge remote-tracking branch 'origin/main' into docs/php-compatibility-matrix

This commit is contained in:
Dominik Chrastecky 2022-03-07 12:42:16 +01:00
commit 29c8f1ec72
45 changed files with 1315 additions and 278 deletions

View File

@ -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:

View File

@ -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.9.0-beta.0",
"keywords": [
"unleash",
"feature toggle",
@ -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",
@ -112,8 +112,9 @@
"serve-favicon": "^2.5.0",
"stoppable": "^1.1.0",
"type-is": "^1.6.18",
"unleash-frontend": "4.8.0-beta.5",
"uuid": "^8.3.2"
"unleash-frontend": "4.9.0-beta.0",
"uuid": "^8.3.2",
"semver": "^7.3.5"
},
"devDependencies": {
"@babel/core": "7.17.5",
@ -121,7 +122,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",
@ -133,15 +134,15 @@
"@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.13.0",
"@typescript-eslint/parser": "5.13.0",
"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.3.0",
"eslint-config-prettier": "8.5.0",
"eslint-plugin-import": "2.25.4",
"eslint-plugin-prettier": "4.0.0",
"faker": "5.5.3",
@ -149,16 +150,16 @@
"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",
"superagent": "7.1.1",
"supertest": "6.2.2",
"ts-jest": "27.1.3",
"ts-node": "10.5.0",
"ts-node": "10.7.0",
"tsc-watch": "4.6.0",
"typescript": "4.5.5"
"typescript": "4.6.2"
},
"resolutions": {
"db-migrate/rc/minimist": "^1.2.5",

View File

@ -247,6 +247,19 @@ export class AccessStore implements IAccessStore {
.delete();
}
async updateUserProjectRole(
userId: number,
roleId: number,
projectId: string,
): Promise<void> {
return this.db(T.ROLE_USER)
.where({
user_id: userId,
project: projectId,
})
.update('role_id', roleId);
}
async removeRolesOfTypeForUser(
userId: number,
roleType: string,

View File

@ -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),

View File

@ -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,62 @@ class ProjectStore implements IProjectStore {
async exists(id: string): Promise<boolean> {
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<IProjectWithCount[]> {
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')
.orderBy('projects.name', 'asc');
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<string, number>(
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<IProject[]> {
const rows = await this.db
.select(COLUMNS)
@ -71,7 +130,7 @@ class ProjectStore implements IProjectStore {
async hasProject(id: string): Promise<boolean> {
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 +267,5 @@ class ProjectStore implements IProjectStore {
};
}
}
export default ProjectStore;
module.exports = ProjectStore;

View File

@ -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;
}
}

View File

@ -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();
});

View File

@ -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<SimpleAuthSettings>(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 });
}
}

View File

@ -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<void> {
const constraint: IConstraint = { ...req.body };
await this.featureService.validateConstraint(constraint);
res.status(204).send();
}
async getStrategy(
req: IAuthRequest<StrategyIdParams, any, any, any>,
res: Response,

View File

@ -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<void> {
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,
);

View File

@ -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();

View File

@ -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',
);
}
});

View File

@ -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();

View File

@ -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]',
);
}
});

View File

@ -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({

View File

@ -220,6 +220,14 @@ export class AccessService {
return this.store.removeUserFromRole(userId, roleId, projectId);
}
async updateUserProjectRole(
userId: number,
roleId: number,
projectId: string,
): Promise<void> {
return this.store.updateUserProjectRole(userId, roleId, projectId);
}
//This actually only exists for testing purposes
async addPermissionToRole(
roleId: number,

View File

@ -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<IUnleashConfig, 'getLogger'>,
) {
@ -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<void> {
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,

View File

@ -8,6 +8,7 @@ import NotFoundError from '../error/notfound-error';
import {
ProjectUserAddedEvent,
ProjectUserRemovedEvent,
ProjectUserUpdateRoleEvent,
PROJECT_CREATED,
PROJECT_DELETED,
PROJECT_UPDATED,
@ -35,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;
@ -100,24 +102,7 @@ export default class ProjectService {
}
async getProjects(query?: IProjectQuery): Promise<IProjectWithCount[]> {
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<IProject> {
@ -326,23 +311,9 @@ export default class ProjectService {
userId: number,
createdBy?: string,
): Promise<void> {
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);
@ -355,6 +326,75 @@ export default class ProjectService {
);
}
async findProjectRole(
projectId: string,
roleId: number,
): Promise<IRoleDescriptor> {
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<void> {
if (currentRole.name === RoleName.OWNER) {
const users = await this.accessService.getProjectUsersForRole(
currentRole.id,
projectId,
);
if (users.length < 2) {
throw new ProjectWithoutOwnerError();
}
}
}
async changeRole(
projectId: string,
roleId: number,
userId: number,
createdBy: string,
): Promise<void> {
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,
);
const role = await this.findProjectRole(projectId, roleId);
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<number> {
return this.store.getMembers(projectId);
}
@ -383,5 +423,3 @@ export default class ProjectService {
};
}
}
module.exports = ProjectService;

View File

@ -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(eventData: {
project: string;
createdBy: string;
data: any;
preData: any;
}) {
super(PROJECT_USER_REMOVED, eventData.createdBy);
const { project, data, preData } = eventData;
this.project = project;
this.data = data;
this.preData = preData;
}
}

View File

@ -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',

View File

@ -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;
}
}

View File

@ -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<IRole, number> {
roleId: number,
projectId?: string,
): Promise<void>;
updateUserProjectRole(
userId: number,
roleId: number,
projectId: string,
): Promise<void>;
removeRolesOfTypeForUser(userId: number, roleType: string): Promise<void>;
addPermissionsToRole(
role_id: number,

View File

@ -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<IProject, string> {
deleteEnvironmentForProject(id: string, environment: string): Promise<void>;
getEnvironmentsForProject(id: string): Promise<string[]>;
getMembers(projectId: string): Promise<number>;
getProjectsWithCounts(query?: IProjectQuery): Promise<IProjectWithCount[]>;
count(): Promise<number>;
getAll(query?: IProjectQuery): Promise<IProject[]>;
}

View File

@ -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];

View File

@ -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`,
);
}
});

View File

@ -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<void> => {
await constraintNumberTypeSchema.validateAsync(value);
};
export const validateString = async (value: unknown): Promise<void> => {
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<void> => {
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`,
);
}
};

View File

@ -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();
};

View File

@ -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();
};

View File

@ -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();
};

View File

@ -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();
};

View File

@ -0,0 +1,7 @@
exports.up = function (db, cb) {
db.runSql('ALTER TABLE roles DROP COLUMN IF EXISTS project', cb);
};
exports.down = function (db, cb) {
db.runSql('ALTER TABLE roles ADD COLUMN IF NOT EXISTS project text', cb);
};

View File

@ -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,
);
};

View File

@ -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: '',

View File

@ -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',

View File

@ -652,3 +652,101 @@ 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 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((u) => u.roleId === memberRole.id);
const testUsers = users.filter((u) => u.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',
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'),
);
});

View File

@ -9,6 +9,14 @@ import {
import { IAvailablePermissions, IPermission } from 'lib/types/model';
class AccessStoreMock implements IAccessStore {
updateUserProjectRole(
userId: number,
roleId: number,
projectId: string,
): Promise<void> {
throw new Error('Method not implemented.');
}
removeUserFromRole(
userId: number,
roleId: number,

View File

@ -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<IProjectWithCount[]> {
return this.projects.map((p) => {
return { ...p, memberCount: 0, featureCount: 0 };
});
}
private createInternal(project: IProjectInsert): IProject {
const newProj: IProject = {
...project,

View File

@ -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 **
@ -611,3 +646,156 @@ http PATCH http://localhost:4242/api/admin/projects/default/features/demo/varian
]
}
```
## 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).
### 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
<details>
<summary>Responses data</summary>
##### 200 OK
The user was added to the project with the specified role. This response has no body.
##### 400 Bad Request
The user already exists in the project and cannot be added again:
``` json
[
{
"msg": "User already has access to project=<projectId>"
}
]
```
</details>
#### 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
```
### 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
<details>
<summary>Responses data</summary>
##### 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 the only user with the `owner` role in the project:
``` json
[
{
"msg": "A project must have at least one owner."
}
]
```
</details>
#### 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
DELETE /api/admin/projects/:projectId/users/:userId/roles/:roleId
```
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
| 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
<details>
<summary>Responses data</summary>
##### 200 OK
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
You tried to remove the only user with the role `owner` in the project:
``` json
[
{
"msg": "A project must have at least one owner."
}
]
```
</details>
#### 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
```

View File

@ -115,7 +115,21 @@ 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**
:::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` | 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` |
**Body**
@ -128,12 +142,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`

View File

@ -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 |
@ -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 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |
@ -116,9 +117,26 @@ 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.
## 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.
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.
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/android_proxy_sdk)

View File

@ -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';

View File

@ -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`.

View File

@ -5,9 +5,15 @@ 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](../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 is done client-side. This means that it is *the client* that decides whether a feature should be enabled or not.
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:
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.
The following activation strategies are bundled with Unleash and always available:
- [Standard](#standard)
- [UserIDs](#userids)

View File

@ -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).

206
yarn.lock
View File

@ -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"
@ -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":
@ -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.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.0"
"@typescript-eslint/type-utils" "5.12.0"
"@typescript-eslint/utils" "5.12.0"
"@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.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.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.0"
"@typescript-eslint/types" "5.12.0"
"@typescript-eslint/typescript-estree" "5.12.0"
"@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.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.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.0"
"@typescript-eslint/visitor-keys" "5.12.0"
"@typescript-eslint/types" "5.13.0"
"@typescript-eslint/visitor-keys" "5.13.0"
"@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.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.0"
"@typescript-eslint/utils" "5.13.0"
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.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.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.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.0"
"@typescript-eslint/visitor-keys" "5.12.0"
"@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.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.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.0"
"@typescript-eslint/types" "5.12.0"
"@typescript-eslint/typescript-estree" "5.12.0"
"@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.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.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.0"
"@typescript-eslint/types" "5.13.0"
eslint-visitor-keys "^3.0.0"
abab@^2.0.3, abab@^2.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"
@ -2736,10 +2731,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.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"
@ -2821,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"
@ -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==
@ -4886,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"
@ -5756,6 +5736,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 +5757,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 +5770,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"
@ -5868,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==
@ -7042,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.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"
@ -7194,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"
@ -7241,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.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.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"