Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Add reviewers for the most sensitive folders
/.github/ egor.zalenski@softeq.com artem.horuzhenko@softeq.com
/.circleci/ egor.zalenski@softeq.com artem.horuzhenko@softeq.com
/redisinsight/api/config/features-config.json viktar.starastsenka@redis.com egor.zalenski@softeq.com artem.horuzhenko@softeq.com
8 changes: 7 additions & 1 deletion redisinsight/api/config/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,12 +194,18 @@ export default {
},
],
connections: {
timeout: parseInt(process.env.CONNECTIONS_TIMEOUT_DEFAULT, 10) || 30 * 1_000 // 30 sec
timeout: parseInt(process.env.CONNECTIONS_TIMEOUT_DEFAULT, 10) || 30 * 1_000, // 30 sec
},
redisStack: {
id: process.env.BUILD_TYPE === 'REDIS_STACK' ? process.env.REDIS_STACK_DATABASE_ID || 'redis-stack' : undefined,
name: process.env.REDIS_STACK_DATABASE_NAME,
host: process.env.REDIS_STACK_DATABASE_HOST,
port: process.env.REDIS_STACK_DATABASE_PORT,
},
features_config: {
url: process.env.RI_FEATURES_CONFIG_URL
// eslint-disable-next-line max-len
|| 'https://raw.githubusercontent.com/RedisInsight/RedisInsight/main/redisinsight/api/config/features-config.json',
syncInterval: parseInt(process.env.RI_FEATURES_CONFIG_SYNC_INTERVAL, 10) || 1_000 * 60 * 60 * 4, // 4h
},
};
21 changes: 21 additions & 0 deletions redisinsight/api/config/features-config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"version": 1,
"features": {
"insightsRecommendations": {
"flag": true,
"perc": [],
"filters": [
{
"name": "agreements.analytics",
"value": true,
"cond": "eq"
},
{
"name": "config.server.buildType",
"value": "ELECTRON",
"cond": "eq"
}
]
}
}
}
4 changes: 4 additions & 0 deletions redisinsight/api/config/ormconfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import { DatabaseEntity } from 'src/modules/database/entities/database.entity';
import { SshOptionsEntity } from 'src/modules/ssh/entities/ssh-options.entity';
import { BrowserHistoryEntity } from 'src/modules/browser/entities/browser-history.entity';
import { CustomTutorialEntity } from 'src/modules/custom-tutorial/entities/custom-tutorial.entity';
import { FeatureEntity } from 'src/modules/feature/entities/feature.entity';
import { FeaturesConfigEntity } from 'src/modules/feature/entities/features-config.entity';
import migrations from '../migration';
import * as config from '../src/utils/config';

Expand All @@ -40,6 +42,8 @@ const ormConfig = {
BrowserHistoryEntity,
SshOptionsEntity,
CustomTutorialEntity,
FeatureEntity,
FeaturesConfigEntity,
],
migrations,
};
Expand Down
13 changes: 11 additions & 2 deletions redisinsight/api/config/test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
export default {
server: {
env: 'test',
requestTimeout: 1000,
requestTimeout: parseInt(process.env.REQUEST_TIMEOUT, 10) || 1000,
},
db: {
synchronize: process.env.DB_SYNC ? process.env.DB_SYNC === 'true' : true,
migrationsRun: process.env.DB_MIGRATIONS ? process.env.DB_MIGRATIONS === 'true' : false,
},
profiler: {
logFileIdleThreshold: parseInt(process.env.PROFILER_LOG_FILE_IDLE_THRESHOLD, 10) || 1000 * 2, // 3sec
},
notifications: {
updateUrl: 'https://s3.amazonaws.com/redisinsight.test/public/tests/notifications.json',
updateUrl: process.env.NOTIFICATION_UPDATE_URL
|| 'https://s3.amazonaws.com/redisinsight.test/public/tests/notifications.json',
},
features_config: {
url: process.env.RI_FEATURES_CONFIG_URL
|| 'http://localhost:5551/remote/features-config.json',
},
};
16 changes: 16 additions & 0 deletions redisinsight/api/migration/1684175820824-feature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { MigrationInterface, QueryRunner } from "typeorm";

export class feature1684175820824 implements MigrationInterface {
name = 'feature1684175820824'

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "features" ("name" varchar PRIMARY KEY NOT NULL, "flag" boolean NOT NULL)`);
await queryRunner.query(`CREATE TABLE "features_config" ("id" varchar PRIMARY KEY NOT NULL, "controlNumber" integer, "data" varchar NOT NULL, "updatedAt" datetime NOT NULL DEFAULT (datetime('now')))`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "features_config"`);
await queryRunner.query(`DROP TABLE "features"`);
}

}
2 changes: 2 additions & 0 deletions redisinsight/api/migration/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { databaseCompressor1678182722874 } from './1678182722874-database-compre
import { customTutorials1677135091633 } from './1677135091633-custom-tutorials';
import { databaseRecommendations1681900503586 } from './1681900503586-database-recommendations';
import { databaseRecommendationParams1683006064293 } from './1683006064293-database-recommendation-params';
import { feature1684175820824 } from './1684175820824-feature';

export default [
initialMigration1614164490968,
Expand Down Expand Up @@ -66,4 +67,5 @@ export default [
customTutorials1677135091633,
databaseRecommendations1681900503586,
databaseRecommendationParams1683006064293,
feature1684175820824,
];
2 changes: 1 addition & 1 deletion redisinsight/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand -w 1",
"test:e2e": "jest --config ./test/jest-e2e.json -w 1",
"typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js -d ./config/ormconfig.ts",
"test:api": "ts-mocha --paths -p test/api/api.tsconfig.json --config ./test/api/.mocharc.yml",
"test:api": "cross-env NODE_ENV=test ts-mocha --paths -p test/api/api.tsconfig.json --config ./test/api/.mocharc.yml",
"test:api:cov": "nyc --reporter=html --reporter=text --reporter=text-summary yarn run test:api",
"test:api:ci:cov": "nyc -r text -r text-summary -r html yarn run test:api --reporter mocha-multi-reporters --reporter-options configFile=test/api/reporters.json && nyc merge .nyc_output ./coverage/test-run-coverage.json",
"typeorm:migrate": "cross-env NODE_ENV=staging yarn typeorm migration:generate ./migration/migration",
Expand Down
1 change: 1 addition & 0 deletions redisinsight/api/src/__mocks__/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export const mockRepository = jest.fn(() => ({
save: jest.fn(),
insert: jest.fn(),
update: jest.fn(),
upsert: jest.fn(),
delete: jest.fn(),
remove: jest.fn(),
createQueryBuilder: mockCreateQueryBuilder,
Expand Down
219 changes: 219 additions & 0 deletions redisinsight/api/src/__mocks__/feature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
import { FeaturesConfigEntity } from 'src/modules/feature/entities/features-config.entity';
import {
FeatureConfig,
FeatureConfigFilter, FeatureConfigFilterAnd, FeatureConfigFilterOr,
FeaturesConfig,
FeaturesConfigData,
} from 'src/modules/feature/model/features-config';
import { classToClass } from 'src/utils';
import { Feature } from 'src/modules/feature/model/feature';
import { FeatureEntity } from 'src/modules/feature/entities/feature.entity';
import { mockAppSettings } from 'src/__mocks__/app-settings';
import config from 'src/utils/config';
import { KnownFeatures } from 'src/modules/feature/constants';
import * as defaultConfig from '../../config/features-config.json';

export const mockFeaturesConfigId = '1';
export const mockFeaturesConfigVersion = defaultConfig.version + 0.111;
export const mockControlNumber = 7.68;
export const mockControlGroup = '7';

export const mockFeaturesConfigJson = {
version: mockFeaturesConfigVersion,
features: {
[KnownFeatures.InsightsRecommendations]: {
perc: [[1.25, 8.45]],
flag: true,
filters: [
{
name: 'agreements.analytics',
value: true,
cond: 'eq',
},
],
},
},
};

export const mockFeaturesConfigJsonComplex = {
...mockFeaturesConfigJson,
features: {
[KnownFeatures.InsightsRecommendations]: {
...mockFeaturesConfigJson.features[KnownFeatures.InsightsRecommendations],
filters: [
{
or: [
{
name: 'settings.testValue',
value: 'test',
cond: 'eq',
},
{
and: [
{
name: 'agreements.analytics',
value: true,
cond: 'eq',
},
{
or: [
{
name: 'settings.scanThreshold',
value: mockAppSettings.scanThreshold,
cond: 'eq',
},
{
name: 'settings.batchSize',
value: mockAppSettings.batchSize,
cond: 'eq',
},
],
},
],
},
],
},
],
},
},
};

export const mockFeaturesConfigData = Object.assign(new FeaturesConfigData(), {
...mockFeaturesConfigJson,
features: new Map(Object.entries({
[KnownFeatures.InsightsRecommendations]: Object.assign(new FeatureConfig(), {
...mockFeaturesConfigJson.features[KnownFeatures.InsightsRecommendations],
filters: [
Object.assign(new FeatureConfigFilter(), {
...mockFeaturesConfigJson.features[KnownFeatures.InsightsRecommendations].filters[0],
}),
],
}),
})),
});

export const mockFeaturesConfigDataComplex = Object.assign(new FeaturesConfigData(), {
...mockFeaturesConfigJson,
features: new Map(Object.entries({
[KnownFeatures.InsightsRecommendations]: Object.assign(new FeatureConfig(), {
...mockFeaturesConfigJson.features[KnownFeatures.InsightsRecommendations],
filters: [
Object.assign(new FeatureConfigFilterOr(), {
or: [
Object.assign(new FeatureConfigFilter(), {
name: 'settings.testValue',
value: 'test',
cond: 'eq',
}),
Object.assign(new FeatureConfigFilterAnd(), {
and: [
Object.assign(new FeatureConfigFilter(), {
name: 'agreements.analytics',
value: true,
cond: 'eq',
}),
Object.assign(new FeatureConfigFilterOr(), {
or: [
Object.assign(new FeatureConfigFilter(), {
name: 'settings.scanThreshold',
value: mockAppSettings.scanThreshold,
cond: 'eq',
}),
Object.assign(new FeatureConfigFilter(), {
name: 'settings.batchSize',
value: mockAppSettings.batchSize,
cond: 'eq',
}),
],
}),
],
}),
],
}),
],
}),
})),
});

export const mockFeaturesConfig = Object.assign(new FeaturesConfig(), {
controlNumber: mockControlNumber,
data: mockFeaturesConfigData,
});

export const mockFeaturesConfigComplex = Object.assign(new FeaturesConfig(), {
controlNumber: mockControlNumber,
data: mockFeaturesConfigDataComplex,
});

export const mockFeaturesConfigEntity = Object.assign(new FeaturesConfigEntity(), {
...classToClass(FeaturesConfigEntity, mockFeaturesConfig),
id: mockFeaturesConfigId,
});

export const mockFeaturesConfigEntityComplex = Object.assign(new FeaturesConfigEntity(), {
...classToClass(FeaturesConfigEntity, mockFeaturesConfigComplex),
id: mockFeaturesConfigId,
});

export const mockFeature = Object.assign(new Feature(), {
name: KnownFeatures.InsightsRecommendations,
flag: true,
});

export const mockUnknownFeature = Object.assign(new Feature(), {
name: 'unknown',
flag: true,
});

export const mockFeatureEntity = Object.assign(new FeatureEntity(), {
id: 'lr-1',
name: KnownFeatures.InsightsRecommendations,
flag: true,
});

export const mockServerState = {
settings: mockAppSettings,
agreements: mockAppSettings.agreements,
config: config.get(),
env: process.env,
};

export const mockFeaturesConfigRepository = jest.fn(() => ({
getOrCreate: jest.fn().mockResolvedValue(mockFeaturesConfig),
update: jest.fn().mockResolvedValue(mockFeaturesConfig),
}));

export const mockFeatureRepository = jest.fn(() => ({
get: jest.fn().mockResolvedValue(mockFeature),
upsert: jest.fn().mockResolvedValue({ updated: 1 }),
list: jest.fn().mockResolvedValue([mockFeature]),
delete: jest.fn().mockResolvedValue({ deleted: 1 }),
}));

export const mockFeaturesConfigService = jest.fn(() => ({
sync: jest.fn(),
getControlInfo: jest.fn().mockResolvedValue({
controlNumber: mockControlNumber,
controlGroup: mockControlGroup,
}),
}));

export const mockFeatureService = jest.fn(() => ({
isFeatureEnabled: jest.fn().mockResolvedValue(true),
}));

export const mockFeatureAnalytics = jest.fn(() => ({
sendFeatureFlagConfigUpdated: jest.fn(),
sendFeatureFlagConfigUpdateError: jest.fn(),
sendFeatureFlagInvalidRemoteConfig: jest.fn(),
sendFeatureFlagRecalculated: jest.fn(),
}));

export const mockInsightsRecommendationsFlagStrategy = {
calculate: jest.fn().mockResolvedValue(true),
};

export const mockFeatureFlagProvider = jest.fn(() => ({
getStrategy: jest.fn().mockResolvedValue(mockInsightsRecommendationsFlagStrategy),
calculate: jest.fn().mockResolvedValue(true),
}));
1 change: 1 addition & 0 deletions redisinsight/api/src/__mocks__/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ export * from './redis-client';
export * from './ssh';
export * from './browser-history';
export * from './database-recommendation';
export * from './feature';
2 changes: 2 additions & 0 deletions redisinsight/api/src/common/decorators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ export * from './default';
export * from './data-as-json-string.decorator';
export * from './session';
export * from './client-metadata';
export * from './object-as-map.decorator';
export * from './is-multi-number.decorator';
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {
registerDecorator,
ValidationOptions,
} from 'class-validator';
import { MultiNumberValidator } from 'src/common/validators';

export function IsMultiNumber(validationOptions?: ValidationOptions) {
return (object: any, propertyName: string) => {
registerDecorator({
name: 'IsMultiNumber',
target: object.constructor,
propertyName,
options: validationOptions,
validator: MultiNumberValidator,
});
};
}
Loading