mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-14 00:19:16 +01:00
Favorite project (#2569)
Adds ability to favorite projects. 1. Can favorite project 2. Can unfavorite project 3. Favorite field is returned on `/api/admin/projects/default` 4. Favorite field is returned on` /api/admin/projects`
This commit is contained in:
parent
fab6fbb756
commit
a22d5f5a43
96
src/lib/db/favorite-projects-store.ts
Normal file
96
src/lib/db/favorite-projects-store.ts
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
import EventEmitter from 'events';
|
||||||
|
import { Logger, LogProvider } from '../logger';
|
||||||
|
import { Knex } from 'knex';
|
||||||
|
import { IFavoriteProject } from '../types/favorites';
|
||||||
|
import {
|
||||||
|
IFavoriteProjectKey,
|
||||||
|
IFavoriteProjectsStore,
|
||||||
|
} from '../types/stores/favorite-projects';
|
||||||
|
|
||||||
|
const T = {
|
||||||
|
FAVORITE_PROJECTS: 'favorite_projects',
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IFavoriteProjectRow {
|
||||||
|
user_id: number;
|
||||||
|
project: string;
|
||||||
|
created_at: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowToFavorite = (row: IFavoriteProjectRow) => {
|
||||||
|
return {
|
||||||
|
userId: row.user_id,
|
||||||
|
project: row.project,
|
||||||
|
createdAt: row.created_at,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export class FavoriteProjectsStore implements IFavoriteProjectsStore {
|
||||||
|
private logger: Logger;
|
||||||
|
|
||||||
|
private eventBus: EventEmitter;
|
||||||
|
|
||||||
|
private db: Knex;
|
||||||
|
|
||||||
|
constructor(db: Knex, eventBus: EventEmitter, getLogger: LogProvider) {
|
||||||
|
this.db = db;
|
||||||
|
this.eventBus = eventBus;
|
||||||
|
this.logger = getLogger('lib/db/favorites-store.ts');
|
||||||
|
}
|
||||||
|
|
||||||
|
async addFavoriteProject({
|
||||||
|
userId,
|
||||||
|
project,
|
||||||
|
}: IFavoriteProjectKey): Promise<IFavoriteProject> {
|
||||||
|
const insertedProject = await this.db<IFavoriteProjectRow>(
|
||||||
|
T.FAVORITE_PROJECTS,
|
||||||
|
)
|
||||||
|
.insert({ project, user_id: userId })
|
||||||
|
.onConflict(['user_id', 'project'])
|
||||||
|
.merge()
|
||||||
|
.returning('*');
|
||||||
|
|
||||||
|
return rowToFavorite(insertedProject[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async delete({ userId, project }: IFavoriteProjectKey): Promise<void> {
|
||||||
|
return this.db(T.FAVORITE_PROJECTS)
|
||||||
|
.where({ project, user_id: userId })
|
||||||
|
.del();
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteAll(): Promise<void> {
|
||||||
|
await this.db(T.FAVORITE_PROJECTS).del();
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {}
|
||||||
|
|
||||||
|
async exists({ userId, project }: IFavoriteProjectKey): Promise<boolean> {
|
||||||
|
const result = await this.db.raw(
|
||||||
|
`SELECT EXISTS(SELECT 1 FROM ${T.FAVORITE_PROJECTS} WHERE user_id = ? AND project = ?) AS present`,
|
||||||
|
[userId, project],
|
||||||
|
);
|
||||||
|
const { present } = result.rows[0];
|
||||||
|
return present;
|
||||||
|
}
|
||||||
|
|
||||||
|
async get({
|
||||||
|
userId,
|
||||||
|
project,
|
||||||
|
}: IFavoriteProjectKey): Promise<IFavoriteProject> {
|
||||||
|
const favorite = await this.db
|
||||||
|
.table<IFavoriteProjectRow>(T.FAVORITE_PROJECTS)
|
||||||
|
.select()
|
||||||
|
.where({ project, user_id: userId })
|
||||||
|
.first();
|
||||||
|
|
||||||
|
return rowToFavorite(favorite);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAll(): Promise<IFavoriteProject[]> {
|
||||||
|
const groups = await this.db<IFavoriteProjectRow>(
|
||||||
|
T.FAVORITE_PROJECTS,
|
||||||
|
).select();
|
||||||
|
return groups.map(rowToFavorite);
|
||||||
|
}
|
||||||
|
}
|
@ -22,6 +22,7 @@ import { ensureStringValue } from '../util/ensureStringValue';
|
|||||||
import { mapValues } from '../util/map-values';
|
import { mapValues } from '../util/map-values';
|
||||||
import { IFlagResolver } from '../types/experimental';
|
import { IFlagResolver } from '../types/experimental';
|
||||||
import { IFeatureProjectUserParams } from '../routes/admin-api/project/features';
|
import { IFeatureProjectUserParams } from '../routes/admin-api/project/features';
|
||||||
|
import Raw = Knex.Raw;
|
||||||
|
|
||||||
const COLUMNS = [
|
const COLUMNS = [
|
||||||
'id',
|
'id',
|
||||||
@ -257,7 +258,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
|||||||
.where('name', featureName)
|
.where('name', featureName)
|
||||||
.modify(FeatureToggleStore.filterByArchived, archived);
|
.modify(FeatureToggleStore.filterByArchived, archived);
|
||||||
|
|
||||||
let selectColumns = ['features_view.*'];
|
let selectColumns = ['features_view.*'] as (string | Raw<any>)[];
|
||||||
if (userId && this.flagResolver.isEnabled('favorites')) {
|
if (userId && this.flagResolver.isEnabled('favorites')) {
|
||||||
query = query.leftJoin(`favorite_features`, function () {
|
query = query.leftJoin(`favorite_features`, function () {
|
||||||
this.on(
|
this.on(
|
||||||
@ -267,7 +268,9 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
|||||||
});
|
});
|
||||||
selectColumns = [
|
selectColumns = [
|
||||||
...selectColumns,
|
...selectColumns,
|
||||||
'favorite_features.feature as favorite',
|
this.db.raw(
|
||||||
|
'favorite_features.feature is not null as favorite',
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
const rows = await query.select(selectColumns);
|
const rows = await query.select(selectColumns);
|
||||||
@ -279,7 +282,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
acc.name = r.name;
|
acc.name = r.name;
|
||||||
acc.favorite = r.favorite != null;
|
acc.favorite = r.favorite;
|
||||||
acc.impressionData = r.impression_data;
|
acc.impressionData = r.impression_data;
|
||||||
acc.description = r.description;
|
acc.description = r.description;
|
||||||
acc.project = r.project;
|
acc.project = r.project;
|
||||||
@ -428,7 +431,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
|||||||
'feature_environments.environment as environment',
|
'feature_environments.environment as environment',
|
||||||
'environments.type as environment_type',
|
'environments.type as environment_type',
|
||||||
'environments.sort_order as environment_sort_order',
|
'environments.sort_order as environment_sort_order',
|
||||||
];
|
] as (string | Raw<any>)[];
|
||||||
|
|
||||||
if (this.flagResolver.isEnabled('toggleTagFiltering')) {
|
if (this.flagResolver.isEnabled('toggleTagFiltering')) {
|
||||||
query = query.leftJoin(
|
query = query.leftJoin(
|
||||||
@ -443,14 +446,19 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
if (userId && this.flagResolver.isEnabled('favorites')) {
|
if (userId && this.flagResolver.isEnabled('favorites')) {
|
||||||
query = query.leftJoin(`favorite_features as ff`, function () {
|
query = query.leftJoin(`favorite_features`, function () {
|
||||||
this.on('ff.feature', 'features.name').andOnVal(
|
this.on('favorite_features.feature', 'features.name').andOnVal(
|
||||||
'ff.user_id',
|
'favorite_features.user_id',
|
||||||
'=',
|
'=',
|
||||||
userId,
|
userId,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
selectColumns = [...selectColumns, 'ff.feature as favorite'];
|
selectColumns = [
|
||||||
|
...selectColumns,
|
||||||
|
this.db.raw(
|
||||||
|
'favorite_features.feature is not null as favorite',
|
||||||
|
),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
query = query.select(selectColumns);
|
query = query.select(selectColumns);
|
||||||
@ -469,7 +477,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
|||||||
} else {
|
} else {
|
||||||
acc[r.feature_name] = {
|
acc[r.feature_name] = {
|
||||||
type: r.type,
|
type: r.type,
|
||||||
favorite: r.favorite != null,
|
favorite: r.favorite,
|
||||||
name: r.feature_name,
|
name: r.feature_name,
|
||||||
createdAt: r.created_at,
|
createdAt: r.created_at,
|
||||||
lastSeenAt: r.last_seen_at,
|
lastSeenAt: r.last_seen_at,
|
||||||
|
@ -16,6 +16,7 @@ import FeatureToggleStore from './feature-toggle-store';
|
|||||||
import { ensureStringValue } from '../util/ensureStringValue';
|
import { ensureStringValue } from '../util/ensureStringValue';
|
||||||
import { mapValues } from '../util/map-values';
|
import { mapValues } from '../util/map-values';
|
||||||
import { IFlagResolver } from '../types/experimental';
|
import { IFlagResolver } from '../types/experimental';
|
||||||
|
import Raw = Knex.Raw;
|
||||||
|
|
||||||
export interface FeaturesTable {
|
export interface FeaturesTable {
|
||||||
name: string;
|
name: string;
|
||||||
@ -101,7 +102,7 @@ export default class FeatureToggleClientStore
|
|||||||
'fs.constraints as constraints',
|
'fs.constraints as constraints',
|
||||||
'segments.id as segment_id',
|
'segments.id as segment_id',
|
||||||
'segments.constraints as segment_constraints',
|
'segments.constraints as segment_constraints',
|
||||||
];
|
] as (string | Raw<any>)[];
|
||||||
|
|
||||||
let query = this.db('features')
|
let query = this.db('features')
|
||||||
.modify(FeatureToggleStore.filterByArchived, archived)
|
.modify(FeatureToggleStore.filterByArchived, archived)
|
||||||
@ -148,14 +149,18 @@ export default class FeatureToggleClientStore
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (userId && this.flagResolver.isEnabled('favorites')) {
|
if (userId && this.flagResolver.isEnabled('favorites')) {
|
||||||
query = query.leftJoin(`favorite_features as ff`, function () {
|
query = query.leftJoin(`favorite_features`, function () {
|
||||||
this.on('ff.feature', 'features.name').andOnVal(
|
this.on(
|
||||||
'ff.user_id',
|
'favorite_features.feature',
|
||||||
'=',
|
'features.name',
|
||||||
userId,
|
).andOnVal('favorite_features.user_id', '=', userId);
|
||||||
);
|
|
||||||
});
|
});
|
||||||
selectColumns = [...selectColumns, 'ff.feature as favorite'];
|
selectColumns = [
|
||||||
|
...selectColumns,
|
||||||
|
this.db.raw(
|
||||||
|
'favorite_features.feature is not null as favorite',
|
||||||
|
),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -207,7 +212,7 @@ export default class FeatureToggleClientStore
|
|||||||
feature.impressionData = r.impression_data;
|
feature.impressionData = r.impression_data;
|
||||||
feature.enabled = !!r.enabled;
|
feature.enabled = !!r.enabled;
|
||||||
feature.name = r.name;
|
feature.name = r.name;
|
||||||
feature.favorite = r.favorite != null;
|
feature.favorite = r.favorite;
|
||||||
feature.description = r.description;
|
feature.description = r.description;
|
||||||
feature.project = r.project;
|
feature.project = r.project;
|
||||||
feature.stale = r.stale;
|
feature.stale = r.stale;
|
||||||
|
@ -33,6 +33,7 @@ import GroupStore from './group-store';
|
|||||||
import PatStore from './pat-store';
|
import PatStore from './pat-store';
|
||||||
import { PublicSignupTokenStore } from './public-signup-token-store';
|
import { PublicSignupTokenStore } from './public-signup-token-store';
|
||||||
import { FavoriteFeaturesStore } from './favorite-features-store';
|
import { FavoriteFeaturesStore } from './favorite-features-store';
|
||||||
|
import { FavoriteProjectsStore } from './favorite-projects-store';
|
||||||
|
|
||||||
export const createStores = (
|
export const createStores = (
|
||||||
config: IUnleashConfig,
|
config: IUnleashConfig,
|
||||||
@ -56,7 +57,12 @@ export const createStores = (
|
|||||||
contextFieldStore: new ContextFieldStore(db, getLogger),
|
contextFieldStore: new ContextFieldStore(db, getLogger),
|
||||||
settingStore: new SettingStore(db, getLogger),
|
settingStore: new SettingStore(db, getLogger),
|
||||||
userStore: new UserStore(db, getLogger),
|
userStore: new UserStore(db, getLogger),
|
||||||
projectStore: new ProjectStore(db, eventBus, getLogger),
|
projectStore: new ProjectStore(
|
||||||
|
db,
|
||||||
|
eventBus,
|
||||||
|
getLogger,
|
||||||
|
config.flagResolver,
|
||||||
|
),
|
||||||
tagStore: new TagStore(db, eventBus, getLogger),
|
tagStore: new TagStore(db, eventBus, getLogger),
|
||||||
tagTypeStore: new TagTypeStore(db, eventBus, getLogger),
|
tagTypeStore: new TagTypeStore(db, eventBus, getLogger),
|
||||||
addonStore: new AddonStore(db, eventBus, getLogger),
|
addonStore: new AddonStore(db, eventBus, getLogger),
|
||||||
@ -100,6 +106,11 @@ export const createStores = (
|
|||||||
eventBus,
|
eventBus,
|
||||||
getLogger,
|
getLogger,
|
||||||
),
|
),
|
||||||
|
favoriteProjectsStore: new FavoriteProjectsStore(
|
||||||
|
db,
|
||||||
|
eventBus,
|
||||||
|
getLogger,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -13,6 +13,8 @@ import { DEFAULT_ENV } from '../util/constants';
|
|||||||
import metricsHelper from '../util/metrics-helper';
|
import metricsHelper from '../util/metrics-helper';
|
||||||
import { DB_TIME } from '../metric-events';
|
import { DB_TIME } from '../metric-events';
|
||||||
import EventEmitter from 'events';
|
import EventEmitter from 'events';
|
||||||
|
import { IFlagResolver } from '../types';
|
||||||
|
import Raw = Knex.Raw;
|
||||||
|
|
||||||
const COLUMNS = [
|
const COLUMNS = [
|
||||||
'id',
|
'id',
|
||||||
@ -39,9 +41,16 @@ class ProjectStore implements IProjectStore {
|
|||||||
|
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
|
|
||||||
|
private flagResolver: IFlagResolver;
|
||||||
|
|
||||||
private timer: Function;
|
private timer: Function;
|
||||||
|
|
||||||
constructor(db: Knex, eventBus: EventEmitter, getLogger: LogProvider) {
|
constructor(
|
||||||
|
db: Knex,
|
||||||
|
eventBus: EventEmitter,
|
||||||
|
getLogger: LogProvider,
|
||||||
|
flagResolver: IFlagResolver,
|
||||||
|
) {
|
||||||
this.db = db;
|
this.db = db;
|
||||||
this.logger = getLogger('project-store.ts');
|
this.logger = getLogger('project-store.ts');
|
||||||
this.timer = (action) =>
|
this.timer = (action) =>
|
||||||
@ -49,6 +58,7 @@ class ProjectStore implements IProjectStore {
|
|||||||
store: 'project',
|
store: 'project',
|
||||||
action,
|
action,
|
||||||
});
|
});
|
||||||
|
this.flagResolver = flagResolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
@ -73,21 +83,43 @@ class ProjectStore implements IProjectStore {
|
|||||||
|
|
||||||
async getProjectsWithCounts(
|
async getProjectsWithCounts(
|
||||||
query?: IProjectQuery,
|
query?: IProjectQuery,
|
||||||
|
userId?: number,
|
||||||
): Promise<IProjectWithCount[]> {
|
): Promise<IProjectWithCount[]> {
|
||||||
const projectTimer = this.timer('getProjectsWithCount');
|
const projectTimer = this.timer('getProjectsWithCount');
|
||||||
let projects = this.db(TABLE)
|
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')
|
.leftJoin('features', 'features.project', 'projects.id')
|
||||||
.groupBy('projects.id')
|
|
||||||
.orderBy('projects.name', 'asc');
|
.orderBy('projects.name', 'asc');
|
||||||
if (query) {
|
if (query) {
|
||||||
projects = projects.where(query);
|
projects = projects.where(query);
|
||||||
}
|
}
|
||||||
const projectAndFeatureCount = await projects;
|
let selectColumns = [
|
||||||
|
this.db.raw(
|
||||||
|
'projects.id, projects.name, projects.description, projects.health, projects.updated_at, count(features.name) AS number_of_features',
|
||||||
|
),
|
||||||
|
] as (string | Raw<any>)[];
|
||||||
|
|
||||||
|
let groupByColumns = ['projects.id'];
|
||||||
|
|
||||||
|
if (userId && this.flagResolver.isEnabled('favorites')) {
|
||||||
|
projects = projects.leftJoin(`favorite_projects`, function () {
|
||||||
|
this.on('favorite_projects.project', 'projects.id').andOnVal(
|
||||||
|
'favorite_projects.user_id',
|
||||||
|
'=',
|
||||||
|
userId,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
selectColumns = [
|
||||||
|
...selectColumns,
|
||||||
|
this.db.raw(
|
||||||
|
'favorite_projects.project is not null as favorite',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
groupByColumns = [...groupByColumns, 'favorite_projects.project'];
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectAndFeatureCount = await projects
|
||||||
|
.select(selectColumns)
|
||||||
|
.groupBy(groupByColumns);
|
||||||
|
|
||||||
const projectsWithFeatureCount = projectAndFeatureCount.map(
|
const projectsWithFeatureCount = projectAndFeatureCount.map(
|
||||||
this.mapProjectWithCountRow,
|
this.mapProjectWithCountRow,
|
||||||
@ -112,6 +144,7 @@ class ProjectStore implements IProjectStore {
|
|||||||
id: row.id,
|
id: row.id,
|
||||||
description: row.description,
|
description: row.description,
|
||||||
health: row.health,
|
health: row.health,
|
||||||
|
favorite: row.favorite,
|
||||||
featureCount: Number(row.number_of_features) || 0,
|
featureCount: Number(row.number_of_features) || 0,
|
||||||
memberCount: Number(row.number_of_users) || 0,
|
memberCount: Number(row.number_of_users) || 0,
|
||||||
updatedAt: row.updated_at,
|
updatedAt: row.updated_at,
|
||||||
|
@ -45,6 +45,9 @@ export const healthOverviewSchema = {
|
|||||||
format: 'date-time',
|
format: 'date-time',
|
||||||
nullable: true,
|
nullable: true,
|
||||||
},
|
},
|
||||||
|
favorite: {
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
schemas: {
|
schemas: {
|
||||||
|
@ -36,6 +36,9 @@ export const projectSchema = {
|
|||||||
changeRequestsEnabled: {
|
changeRequestsEnabled: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
},
|
},
|
||||||
|
favorite: {
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
components: {},
|
components: {},
|
||||||
} as const;
|
} as const;
|
||||||
|
@ -52,6 +52,34 @@ export default class FavoritesController extends Controller {
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.route({
|
||||||
|
method: 'post',
|
||||||
|
path: '/:projectId/favorites',
|
||||||
|
handler: this.addFavoriteProject,
|
||||||
|
permission: NONE,
|
||||||
|
middleware: [
|
||||||
|
openApiService.validPath({
|
||||||
|
tags: ['Features'],
|
||||||
|
operationId: 'addFavoriteProject',
|
||||||
|
responses: { 200: emptyResponse },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
this.route({
|
||||||
|
method: 'delete',
|
||||||
|
path: '/:projectId/favorites',
|
||||||
|
handler: this.removeFavoriteProject,
|
||||||
|
permission: NONE,
|
||||||
|
middleware: [
|
||||||
|
openApiService.validPath({
|
||||||
|
tags: ['Features'],
|
||||||
|
operationId: 'removeFavoriteProject',
|
||||||
|
responses: { 200: emptyResponse },
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async addFavoriteFeature(
|
async addFavoriteFeature(
|
||||||
@ -79,4 +107,30 @@ export default class FavoritesController extends Controller {
|
|||||||
});
|
});
|
||||||
res.status(200).end();
|
res.status(200).end();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async addFavoriteProject(
|
||||||
|
req: IAuthRequest<{ projectId: string }>,
|
||||||
|
res: Response,
|
||||||
|
): Promise<void> {
|
||||||
|
const { projectId } = req.params;
|
||||||
|
const { user } = req;
|
||||||
|
await this.favoritesService.addFavoriteProject({
|
||||||
|
project: projectId,
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
res.status(200).end();
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeFavoriteProject(
|
||||||
|
req: IAuthRequest<{ projectId: string }>,
|
||||||
|
res: Response,
|
||||||
|
): Promise<void> {
|
||||||
|
const { projectId } = req.params;
|
||||||
|
const { user } = req;
|
||||||
|
await this.favoritesService.removeFavoriteProject({
|
||||||
|
project: projectId,
|
||||||
|
userId: user.id,
|
||||||
|
});
|
||||||
|
res.status(200).end();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ import {
|
|||||||
healthReportSchema,
|
healthReportSchema,
|
||||||
HealthReportSchema,
|
HealthReportSchema,
|
||||||
} from '../../../openapi/spec/health-report-schema';
|
} from '../../../openapi/spec/health-report-schema';
|
||||||
|
import { IAuthRequest } from '../../unleash-types';
|
||||||
|
|
||||||
export default class ProjectHealthReport extends Controller {
|
export default class ProjectHealthReport extends Controller {
|
||||||
private projectHealthService: ProjectHealthService;
|
private projectHealthService: ProjectHealthService;
|
||||||
@ -71,14 +72,16 @@ export default class ProjectHealthReport extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getProjectHealthOverview(
|
async getProjectHealthOverview(
|
||||||
req: Request<IProjectParam, unknown, unknown, IArchivedQuery>,
|
req: IAuthRequest<IProjectParam, unknown, unknown, IArchivedQuery>,
|
||||||
res: Response<HealthOverviewSchema>,
|
res: Response<HealthOverviewSchema>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { projectId } = req.params;
|
const { projectId } = req.params;
|
||||||
const { archived } = req.query;
|
const { archived } = req.query;
|
||||||
|
const { user } = req;
|
||||||
const overview = await this.projectHealthService.getProjectOverview(
|
const overview = await this.projectHealthService.getProjectOverview(
|
||||||
projectId,
|
projectId,
|
||||||
archived,
|
archived,
|
||||||
|
user.id,
|
||||||
);
|
);
|
||||||
this.openApiService.respondWithValidation(
|
this.openApiService.respondWithValidation(
|
||||||
200,
|
200,
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Response } from 'express';
|
||||||
import Controller from '../../controller';
|
import Controller from '../../controller';
|
||||||
import { IUnleashConfig } from '../../../types/option';
|
import { IUnleashConfig } from '../../../types/option';
|
||||||
import { IUnleashServices } from '../../../types/services';
|
import { IUnleashServices } from '../../../types/services';
|
||||||
@ -15,6 +15,7 @@ import {
|
|||||||
import { OpenApiService } from '../../../services/openapi-service';
|
import { OpenApiService } from '../../../services/openapi-service';
|
||||||
import { serializeDates } from '../../../types/serialize-dates';
|
import { serializeDates } from '../../../types/serialize-dates';
|
||||||
import { createResponseSchema } from '../../../openapi/util/create-response-schema';
|
import { createResponseSchema } from '../../../openapi/util/create-response-schema';
|
||||||
|
import { IAuthRequest } from '../../unleash-types';
|
||||||
|
|
||||||
export default class ProjectApi extends Controller {
|
export default class ProjectApi extends Controller {
|
||||||
private projectService: ProjectService;
|
private projectService: ProjectService;
|
||||||
@ -49,12 +50,16 @@ export default class ProjectApi extends Controller {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getProjects(
|
async getProjects(
|
||||||
req: Request,
|
req: IAuthRequest,
|
||||||
res: Response<ProjectsSchema>,
|
res: Response<ProjectsSchema>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const projects = await this.projectService.getProjects({
|
const { user } = req;
|
||||||
id: 'default',
|
const projects = await this.projectService.getProjects(
|
||||||
});
|
{
|
||||||
|
id: 'default',
|
||||||
|
},
|
||||||
|
user.id,
|
||||||
|
);
|
||||||
|
|
||||||
this.openApiService.respondWithValidation(
|
this.openApiService.respondWithValidation(
|
||||||
200,
|
200,
|
||||||
|
@ -5,7 +5,11 @@ import {
|
|||||||
IFavoriteFeatureKey,
|
IFavoriteFeatureKey,
|
||||||
IFavoriteFeaturesStore,
|
IFavoriteFeaturesStore,
|
||||||
} from '../types/stores/favorite-features';
|
} from '../types/stores/favorite-features';
|
||||||
import { IFavoriteFeature } from '../types/favorites';
|
import { IFavoriteFeature, IFavoriteProject } from '../types/favorites';
|
||||||
|
import {
|
||||||
|
IFavoriteProjectKey,
|
||||||
|
IFavoriteProjectsStore,
|
||||||
|
} from '../types/stores/favorite-projects';
|
||||||
|
|
||||||
export class FavoritesService {
|
export class FavoritesService {
|
||||||
private config: IUnleashConfig;
|
private config: IUnleashConfig;
|
||||||
@ -14,15 +18,22 @@ export class FavoritesService {
|
|||||||
|
|
||||||
private favoriteFeaturesStore: IFavoriteFeaturesStore;
|
private favoriteFeaturesStore: IFavoriteFeaturesStore;
|
||||||
|
|
||||||
|
private favoriteProjectsStore: IFavoriteProjectsStore;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
{
|
{
|
||||||
favoriteFeaturesStore,
|
favoriteFeaturesStore,
|
||||||
}: Pick<IUnleashStores, 'favoriteFeaturesStore'>,
|
favoriteProjectsStore,
|
||||||
|
}: Pick<
|
||||||
|
IUnleashStores,
|
||||||
|
'favoriteFeaturesStore' | 'favoriteProjectsStore'
|
||||||
|
>,
|
||||||
config: IUnleashConfig,
|
config: IUnleashConfig,
|
||||||
) {
|
) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
this.logger = config.getLogger('services/favorites-service.ts');
|
this.logger = config.getLogger('services/favorites-service.ts');
|
||||||
this.favoriteFeaturesStore = favoriteFeaturesStore;
|
this.favoriteFeaturesStore = favoriteFeaturesStore;
|
||||||
|
this.favoriteProjectsStore = favoriteProjectsStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
async addFavoriteFeature(
|
async addFavoriteFeature(
|
||||||
@ -34,4 +45,27 @@ export class FavoritesService {
|
|||||||
async removeFavoriteFeature(favorite: IFavoriteFeatureKey): Promise<void> {
|
async removeFavoriteFeature(favorite: IFavoriteFeatureKey): Promise<void> {
|
||||||
return this.favoriteFeaturesStore.delete(favorite);
|
return this.favoriteFeaturesStore.delete(favorite);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async addFavoriteProject(
|
||||||
|
favorite: IFavoriteProjectKey,
|
||||||
|
): Promise<IFavoriteProject> {
|
||||||
|
return this.favoriteProjectsStore.addFavoriteProject(favorite);
|
||||||
|
}
|
||||||
|
|
||||||
|
async removeFavoriteProject(favorite: IFavoriteProjectKey): Promise<void> {
|
||||||
|
return this.favoriteProjectsStore.delete(favorite);
|
||||||
|
}
|
||||||
|
|
||||||
|
async isFavoriteProject(
|
||||||
|
projectId: string,
|
||||||
|
userId?: number,
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (userId) {
|
||||||
|
return this.favoriteProjectsStore.exists({
|
||||||
|
project: projectId,
|
||||||
|
userId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -84,10 +84,12 @@ export const createServices = (
|
|||||||
);
|
);
|
||||||
const environmentService = new EnvironmentService(stores, config);
|
const environmentService = new EnvironmentService(stores, config);
|
||||||
const featureTagService = new FeatureTagService(stores, config);
|
const featureTagService = new FeatureTagService(stores, config);
|
||||||
|
const favoritesService = new FavoritesService(stores, config);
|
||||||
const projectHealthService = new ProjectHealthService(
|
const projectHealthService = new ProjectHealthService(
|
||||||
stores,
|
stores,
|
||||||
config,
|
config,
|
||||||
featureToggleServiceV2,
|
featureToggleServiceV2,
|
||||||
|
favoritesService,
|
||||||
);
|
);
|
||||||
const projectService = new ProjectService(
|
const projectService = new ProjectService(
|
||||||
stores,
|
stores,
|
||||||
@ -124,7 +126,6 @@ export const createServices = (
|
|||||||
config,
|
config,
|
||||||
versionService,
|
versionService,
|
||||||
);
|
);
|
||||||
const favoritesService = new FavoritesService(stores, config);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accessService,
|
accessService,
|
||||||
|
@ -14,6 +14,7 @@ import { IProjectStore } from '../types/stores/project-store';
|
|||||||
import FeatureToggleService from './feature-toggle-service';
|
import FeatureToggleService from './feature-toggle-service';
|
||||||
import { hoursToMilliseconds } from 'date-fns';
|
import { hoursToMilliseconds } from 'date-fns';
|
||||||
import Timer = NodeJS.Timer;
|
import Timer = NodeJS.Timer;
|
||||||
|
import { FavoritesService } from './favorites-service';
|
||||||
|
|
||||||
export default class ProjectHealthService {
|
export default class ProjectHealthService {
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
@ -30,6 +31,8 @@ export default class ProjectHealthService {
|
|||||||
|
|
||||||
private featureToggleService: FeatureToggleService;
|
private featureToggleService: FeatureToggleService;
|
||||||
|
|
||||||
|
private favoritesService: FavoritesService;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
{
|
{
|
||||||
projectStore,
|
projectStore,
|
||||||
@ -41,6 +44,7 @@ export default class ProjectHealthService {
|
|||||||
>,
|
>,
|
||||||
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>,
|
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>,
|
||||||
featureToggleService: FeatureToggleService,
|
featureToggleService: FeatureToggleService,
|
||||||
|
favoritesService: FavoritesService,
|
||||||
) {
|
) {
|
||||||
this.logger = getLogger('services/project-health-service.ts');
|
this.logger = getLogger('services/project-health-service.ts');
|
||||||
this.projectStore = projectStore;
|
this.projectStore = projectStore;
|
||||||
@ -52,12 +56,14 @@ export default class ProjectHealthService {
|
|||||||
hoursToMilliseconds(1),
|
hoursToMilliseconds(1),
|
||||||
).unref();
|
).unref();
|
||||||
this.featureToggleService = featureToggleService;
|
this.featureToggleService = featureToggleService;
|
||||||
|
this.favoritesService = favoritesService;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: duplicate from project-service.
|
// TODO: duplicate from project-service.
|
||||||
async getProjectOverview(
|
async getProjectOverview(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
archived: boolean = false,
|
archived: boolean = false,
|
||||||
|
userId?: number,
|
||||||
): Promise<IProjectOverview> {
|
): Promise<IProjectOverview> {
|
||||||
const project = await this.projectStore.get(projectId);
|
const project = await this.projectStore.get(projectId);
|
||||||
const environments = await this.projectStore.getEnvironmentsForProject(
|
const environments = await this.projectStore.getEnvironmentsForProject(
|
||||||
@ -70,10 +76,16 @@ export default class ProjectHealthService {
|
|||||||
const members = await this.projectStore.getMembersCountByProject(
|
const members = await this.projectStore.getMembersCountByProject(
|
||||||
projectId,
|
projectId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const favorite = await this.favoritesService.isFavoriteProject(
|
||||||
|
projectId,
|
||||||
|
userId,
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
name: project.name,
|
name: project.name,
|
||||||
description: project.description,
|
description: project.description,
|
||||||
health: project.health,
|
health: project.health,
|
||||||
|
favorite: favorite,
|
||||||
updatedAt: project.updatedAt,
|
updatedAt: project.updatedAt,
|
||||||
environments,
|
environments,
|
||||||
features,
|
features,
|
||||||
@ -85,7 +97,11 @@ export default class ProjectHealthService {
|
|||||||
async getProjectHealthReport(
|
async getProjectHealthReport(
|
||||||
projectId: string,
|
projectId: string,
|
||||||
): Promise<IProjectHealthReport> {
|
): Promise<IProjectHealthReport> {
|
||||||
const overview = await this.getProjectOverview(projectId, false);
|
const overview = await this.getProjectOverview(
|
||||||
|
projectId,
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
...overview,
|
...overview,
|
||||||
potentiallyStaleCount: await this.potentiallyStaleCount(
|
potentiallyStaleCount: await this.potentiallyStaleCount(
|
||||||
|
@ -120,8 +120,11 @@ export default class ProjectService {
|
|||||||
this.logger = config.getLogger('services/project-service.js');
|
this.logger = config.getLogger('services/project-service.js');
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProjects(query?: IProjectQuery): Promise<IProjectWithCount[]> {
|
async getProjects(
|
||||||
return this.store.getProjectsWithCounts(query);
|
query?: IProjectQuery,
|
||||||
|
userId?: number,
|
||||||
|
): Promise<IProjectWithCount[]> {
|
||||||
|
return this.store.getProjectsWithCounts(query, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getProject(id: string): Promise<IProject> {
|
async getProject(id: string): Promise<IProject> {
|
||||||
|
@ -70,7 +70,6 @@ export interface IFeatureToggleClient {
|
|||||||
lastSeenAt?: Date;
|
lastSeenAt?: Date;
|
||||||
createdAt?: Date;
|
createdAt?: Date;
|
||||||
tags?: ITag[];
|
tags?: ITag[];
|
||||||
|
|
||||||
favorite?: boolean;
|
favorite?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,6 +175,7 @@ export interface IProjectOverview {
|
|||||||
members: number;
|
members: number;
|
||||||
version: number;
|
version: number;
|
||||||
health: number;
|
health: number;
|
||||||
|
favorite?: boolean;
|
||||||
updatedAt?: Date;
|
updatedAt?: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -371,6 +371,7 @@ export interface ICustomRole {
|
|||||||
export interface IProjectWithCount extends IProject {
|
export interface IProjectWithCount extends IProject {
|
||||||
featureCount: number;
|
featureCount: number;
|
||||||
memberCount: number;
|
memberCount: number;
|
||||||
|
favorite?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ISegment {
|
export interface ISegment {
|
||||||
|
@ -29,6 +29,7 @@ import { IGroupStore } from './stores/group-store';
|
|||||||
import { IPatStore } from './stores/pat-store';
|
import { IPatStore } from './stores/pat-store';
|
||||||
import { IPublicSignupTokenStore } from './stores/public-signup-token-store';
|
import { IPublicSignupTokenStore } from './stores/public-signup-token-store';
|
||||||
import { IFavoriteFeaturesStore } from './stores/favorite-features';
|
import { IFavoriteFeaturesStore } from './stores/favorite-features';
|
||||||
|
import { IFavoriteProjectsStore } from './stores/favorite-projects';
|
||||||
|
|
||||||
export interface IUnleashStores {
|
export interface IUnleashStores {
|
||||||
accessStore: IAccessStore;
|
accessStore: IAccessStore;
|
||||||
@ -62,6 +63,7 @@ export interface IUnleashStores {
|
|||||||
patStore: IPatStore;
|
patStore: IPatStore;
|
||||||
publicSignupTokenStore: IPublicSignupTokenStore;
|
publicSignupTokenStore: IPublicSignupTokenStore;
|
||||||
favoriteFeaturesStore: IFavoriteFeaturesStore;
|
favoriteFeaturesStore: IFavoriteFeaturesStore;
|
||||||
|
favoriteProjectsStore: IFavoriteProjectsStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@ -96,4 +98,5 @@ export {
|
|||||||
IUserSplashStore,
|
IUserSplashStore,
|
||||||
IUserStore,
|
IUserStore,
|
||||||
IFavoriteFeaturesStore,
|
IFavoriteFeaturesStore,
|
||||||
|
IFavoriteProjectsStore,
|
||||||
};
|
};
|
||||||
|
14
src/lib/types/stores/favorite-projects.ts
Normal file
14
src/lib/types/stores/favorite-projects.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { IFavoriteProject } from '../favorites';
|
||||||
|
import { Store } from './store';
|
||||||
|
|
||||||
|
export interface IFavoriteProjectKey {
|
||||||
|
userId: number;
|
||||||
|
project: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IFavoriteProjectsStore
|
||||||
|
extends Store<IFavoriteProject, IFavoriteProjectKey> {
|
||||||
|
addFavoriteProject(
|
||||||
|
favorite: IFavoriteProjectKey,
|
||||||
|
): Promise<IFavoriteProject>;
|
||||||
|
}
|
@ -34,21 +34,37 @@ export interface IProjectEnvironmentWithChangeRequests {
|
|||||||
|
|
||||||
export interface IProjectStore extends Store<IProject, string> {
|
export interface IProjectStore extends Store<IProject, string> {
|
||||||
hasProject(id: string): Promise<boolean>;
|
hasProject(id: string): Promise<boolean>;
|
||||||
|
|
||||||
updateHealth(healthUpdate: IProjectHealthUpdate): Promise<void>;
|
updateHealth(healthUpdate: IProjectHealthUpdate): Promise<void>;
|
||||||
|
|
||||||
create(project: IProjectInsert): Promise<IProject>;
|
create(project: IProjectInsert): Promise<IProject>;
|
||||||
|
|
||||||
update(update: IProjectInsert): Promise<void>;
|
update(update: IProjectInsert): Promise<void>;
|
||||||
|
|
||||||
importProjects(
|
importProjects(
|
||||||
projects: IProjectInsert[],
|
projects: IProjectInsert[],
|
||||||
environments?: IEnvironment[],
|
environments?: IEnvironment[],
|
||||||
): Promise<IProject[]>;
|
): Promise<IProject[]>;
|
||||||
|
|
||||||
addEnvironmentToProject(id: string, environment: string): Promise<void>;
|
addEnvironmentToProject(id: string, environment: string): Promise<void>;
|
||||||
|
|
||||||
deleteEnvironmentForProject(id: string, environment: string): Promise<void>;
|
deleteEnvironmentForProject(id: string, environment: string): Promise<void>;
|
||||||
|
|
||||||
getEnvironmentsForProject(id: string): Promise<string[]>;
|
getEnvironmentsForProject(id: string): Promise<string[]>;
|
||||||
|
|
||||||
getMembersCountByProject(projectId: string): Promise<number>;
|
getMembersCountByProject(projectId: string): Promise<number>;
|
||||||
|
|
||||||
getProjectsByUser(userId: number): Promise<string[]>;
|
getProjectsByUser(userId: number): Promise<string[]>;
|
||||||
|
|
||||||
getMembersCount(): Promise<IProjectMembersCount[]>;
|
getMembersCount(): Promise<IProjectMembersCount[]>;
|
||||||
getProjectsWithCounts(query?: IProjectQuery): Promise<IProjectWithCount[]>;
|
|
||||||
|
getProjectsWithCounts(
|
||||||
|
query?: IProjectQuery,
|
||||||
|
userId?: number,
|
||||||
|
): Promise<IProjectWithCount[]>;
|
||||||
|
|
||||||
count(): Promise<number>;
|
count(): Promise<number>;
|
||||||
|
|
||||||
getAll(query?: IProjectQuery): Promise<IProject[]>;
|
getAll(query?: IProjectQuery): Promise<IProject[]>;
|
||||||
|
|
||||||
getProjectLinksForEnvironments(
|
getProjectLinksForEnvironments(
|
||||||
|
@ -51,6 +51,34 @@ const unfavoriteFeature = async (featureName: string) => {
|
|||||||
.expect(200);
|
.expect(200);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const favoriteProject = async (projectName = 'default') => {
|
||||||
|
await app.request
|
||||||
|
.post(`/api/admin/projects/${projectName}/favorites`)
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.expect(200);
|
||||||
|
};
|
||||||
|
|
||||||
|
const unfavoriteProject = async (projectName = 'default') => {
|
||||||
|
await app.request
|
||||||
|
.delete(`/api/admin/projects/${projectName}/favorites`)
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.expect(200);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProject = async (projectName = 'default') => {
|
||||||
|
return app.request
|
||||||
|
.get(`/api/admin/projects/${projectName}`)
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.expect(200);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getProjects = async () => {
|
||||||
|
return app.request
|
||||||
|
.get(`/api/admin/projects`)
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.expect(200);
|
||||||
|
};
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
db = await dbInit('favorites_api_serial', getLogger);
|
db = await dbInit('favorites_api_serial', getLogger);
|
||||||
app = await setupAppWithAuth(db.stores);
|
app = await setupAppWithAuth(db.stores);
|
||||||
@ -148,7 +176,6 @@ test('should be favorited in project single feature endpoint', async () => {
|
|||||||
test('should be able to unfavorite feature', async () => {
|
test('should be able to unfavorite feature', async () => {
|
||||||
const featureName = 'test-feature';
|
const featureName = 'test-feature';
|
||||||
await createFeature(featureName);
|
await createFeature(featureName);
|
||||||
|
|
||||||
await favoriteFeature(featureName);
|
await favoriteFeature(featureName);
|
||||||
await unfavoriteFeature(featureName);
|
await unfavoriteFeature(featureName);
|
||||||
|
|
||||||
@ -162,3 +189,38 @@ test('should be able to unfavorite feature', async () => {
|
|||||||
favorite: false,
|
favorite: false,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should be favorited in projects list', async () => {
|
||||||
|
await favoriteProject();
|
||||||
|
|
||||||
|
const { body } = await getProjects();
|
||||||
|
|
||||||
|
expect(body.projects).toHaveLength(1);
|
||||||
|
expect(body.projects[0]).toMatchObject({
|
||||||
|
name: 'Default',
|
||||||
|
favorite: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should be favorited in single project endpoint', async () => {
|
||||||
|
await favoriteProject();
|
||||||
|
|
||||||
|
const { body } = await getProject();
|
||||||
|
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
name: 'Default',
|
||||||
|
favorite: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('project should not be favorited by default', async () => {
|
||||||
|
await favoriteProject();
|
||||||
|
await unfavoriteProject();
|
||||||
|
|
||||||
|
const { body } = await getProject();
|
||||||
|
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
name: 'Default',
|
||||||
|
favorite: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
@ -1549,6 +1549,9 @@ exports[`should serve the OpenAPI spec 1`] = `
|
|||||||
},
|
},
|
||||||
"type": "array",
|
"type": "array",
|
||||||
},
|
},
|
||||||
|
"favorite": {
|
||||||
|
"type": "boolean",
|
||||||
|
},
|
||||||
"features": {
|
"features": {
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/components/schemas/featureSchema",
|
"$ref": "#/components/schemas/featureSchema",
|
||||||
@ -1594,6 +1597,9 @@ exports[`should serve the OpenAPI spec 1`] = `
|
|||||||
},
|
},
|
||||||
"type": "array",
|
"type": "array",
|
||||||
},
|
},
|
||||||
|
"favorite": {
|
||||||
|
"type": "boolean",
|
||||||
|
},
|
||||||
"features": {
|
"features": {
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/components/schemas/featureSchema",
|
"$ref": "#/components/schemas/featureSchema",
|
||||||
@ -2352,6 +2358,9 @@ exports[`should serve the OpenAPI spec 1`] = `
|
|||||||
"description": {
|
"description": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
},
|
},
|
||||||
|
"favorite": {
|
||||||
|
"type": "boolean",
|
||||||
|
},
|
||||||
"featureCount": {
|
"featureCount": {
|
||||||
"type": "number",
|
"type": "number",
|
||||||
},
|
},
|
||||||
@ -5028,6 +5037,50 @@ If the provided project does not exist, the list of events will be empty.",
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"/api/admin/projects/{projectId}/favorites": {
|
||||||
|
"delete": {
|
||||||
|
"operationId": "removeFavoriteProject",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "projectId",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "This response has no body.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Features",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"operationId": "addFavoriteProject",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"in": "path",
|
||||||
|
"name": "projectId",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "This response has no body.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Features",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
"/api/admin/projects/{projectId}/features": {
|
"/api/admin/projects/{projectId}/features": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "getFeatures",
|
"operationId": "getFeatures",
|
||||||
|
@ -9,6 +9,7 @@ import { IUnleashStores } from '../../../lib/types';
|
|||||||
import { IUser } from '../../../lib/server-impl';
|
import { IUser } from '../../../lib/server-impl';
|
||||||
import { SegmentService } from '../../../lib/services/segment-service';
|
import { SegmentService } from '../../../lib/services/segment-service';
|
||||||
import { GroupService } from '../../../lib/services/group-service';
|
import { GroupService } from '../../../lib/services/group-service';
|
||||||
|
import { FavoritesService } from '../../../lib/services';
|
||||||
|
|
||||||
let stores: IUnleashStores;
|
let stores: IUnleashStores;
|
||||||
let db: ITestDb;
|
let db: ITestDb;
|
||||||
@ -17,6 +18,7 @@ let groupService;
|
|||||||
let accessService;
|
let accessService;
|
||||||
let projectHealthService;
|
let projectHealthService;
|
||||||
let featureToggleService;
|
let featureToggleService;
|
||||||
|
let favoritesService;
|
||||||
let user: IUser;
|
let user: IUser;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
@ -42,10 +44,12 @@ beforeAll(async () => {
|
|||||||
featureToggleService,
|
featureToggleService,
|
||||||
groupService,
|
groupService,
|
||||||
);
|
);
|
||||||
|
favoritesService = new FavoritesService(stores, config);
|
||||||
projectHealthService = new ProjectHealthService(
|
projectHealthService = new ProjectHealthService(
|
||||||
stores,
|
stores,
|
||||||
config,
|
config,
|
||||||
featureToggleService,
|
featureToggleService,
|
||||||
|
favoritesService,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
35
src/test/fixtures/fake-favorite-projects-store.ts
vendored
Normal file
35
src/test/fixtures/fake-favorite-projects-store.ts
vendored
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { IFavoriteProjectsStore } from '../../lib/types';
|
||||||
|
import { IFavoriteProjectKey } from '../../lib/types/stores/favorite-projects';
|
||||||
|
import { IFavoriteProject } from '../../lib/types/favorites';
|
||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
export default class FakeFavoriteProjectsStore
|
||||||
|
implements IFavoriteProjectsStore
|
||||||
|
{
|
||||||
|
addFavoriteProject(
|
||||||
|
favorite: IFavoriteProjectKey,
|
||||||
|
): Promise<IFavoriteProject> {
|
||||||
|
return Promise.resolve(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(key: IFavoriteProjectKey): Promise<void> {
|
||||||
|
return Promise.resolve(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteAll(): Promise<void> {
|
||||||
|
return Promise.resolve(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {}
|
||||||
|
|
||||||
|
exists(key: IFavoriteProjectKey): Promise<boolean> {
|
||||||
|
return Promise.resolve(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
get(key: IFavoriteProjectKey): Promise<IFavoriteProject> {
|
||||||
|
return Promise.resolve(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
getAll(query?: Object): Promise<IFavoriteProject[]> {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
}
|
||||||
|
}
|
2
src/test/fixtures/store.ts
vendored
2
src/test/fixtures/store.ts
vendored
@ -30,6 +30,7 @@ import FakeGroupStore from './fake-group-store';
|
|||||||
import FakePatStore from './fake-pat-store';
|
import FakePatStore from './fake-pat-store';
|
||||||
import FakePublicSignupStore from './fake-public-signup-store';
|
import FakePublicSignupStore from './fake-public-signup-store';
|
||||||
import FakeFavoriteFeaturesStore from './fake-favorite-features-store';
|
import FakeFavoriteFeaturesStore from './fake-favorite-features-store';
|
||||||
|
import FakeFavoriteProjectsStore from './fake-favorite-projects-store';
|
||||||
|
|
||||||
const createStores: () => IUnleashStores = () => {
|
const createStores: () => IUnleashStores = () => {
|
||||||
const db = {
|
const db = {
|
||||||
@ -71,6 +72,7 @@ const createStores: () => IUnleashStores = () => {
|
|||||||
patStore: new FakePatStore(),
|
patStore: new FakePatStore(),
|
||||||
publicSignupTokenStore: new FakePublicSignupStore(),
|
publicSignupTokenStore: new FakePublicSignupStore(),
|
||||||
favoriteFeaturesStore: new FakeFavoriteFeaturesStore(),
|
favoriteFeaturesStore: new FakeFavoriteFeaturesStore(),
|
||||||
|
favoriteProjectsStore: new FakeFavoriteProjectsStore(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user