1
0
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:
sjaanus 2022-11-30 12:41:53 +01:00 committed by GitHub
parent fab6fbb756
commit a22d5f5a43
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 507 additions and 42 deletions

View 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);
}
}

View File

@ -22,6 +22,7 @@ import { ensureStringValue } from '../util/ensureStringValue';
import { mapValues } from '../util/map-values';
import { IFlagResolver } from '../types/experimental';
import { IFeatureProjectUserParams } from '../routes/admin-api/project/features';
import Raw = Knex.Raw;
const COLUMNS = [
'id',
@ -257,7 +258,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
.where('name', featureName)
.modify(FeatureToggleStore.filterByArchived, archived);
let selectColumns = ['features_view.*'];
let selectColumns = ['features_view.*'] as (string | Raw<any>)[];
if (userId && this.flagResolver.isEnabled('favorites')) {
query = query.leftJoin(`favorite_features`, function () {
this.on(
@ -267,7 +268,9 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
});
selectColumns = [
...selectColumns,
'favorite_features.feature as favorite',
this.db.raw(
'favorite_features.feature is not null as favorite',
),
];
}
const rows = await query.select(selectColumns);
@ -279,7 +282,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
}
acc.name = r.name;
acc.favorite = r.favorite != null;
acc.favorite = r.favorite;
acc.impressionData = r.impression_data;
acc.description = r.description;
acc.project = r.project;
@ -428,7 +431,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
'feature_environments.environment as environment',
'environments.type as environment_type',
'environments.sort_order as environment_sort_order',
];
] as (string | Raw<any>)[];
if (this.flagResolver.isEnabled('toggleTagFiltering')) {
query = query.leftJoin(
@ -443,14 +446,19 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
];
}
if (userId && this.flagResolver.isEnabled('favorites')) {
query = query.leftJoin(`favorite_features as ff`, function () {
this.on('ff.feature', 'features.name').andOnVal(
'ff.user_id',
query = query.leftJoin(`favorite_features`, function () {
this.on('favorite_features.feature', 'features.name').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',
),
];
}
query = query.select(selectColumns);
@ -469,7 +477,7 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
} else {
acc[r.feature_name] = {
type: r.type,
favorite: r.favorite != null,
favorite: r.favorite,
name: r.feature_name,
createdAt: r.created_at,
lastSeenAt: r.last_seen_at,

View File

@ -16,6 +16,7 @@ import FeatureToggleStore from './feature-toggle-store';
import { ensureStringValue } from '../util/ensureStringValue';
import { mapValues } from '../util/map-values';
import { IFlagResolver } from '../types/experimental';
import Raw = Knex.Raw;
export interface FeaturesTable {
name: string;
@ -101,7 +102,7 @@ export default class FeatureToggleClientStore
'fs.constraints as constraints',
'segments.id as segment_id',
'segments.constraints as segment_constraints',
];
] as (string | Raw<any>)[];
let query = this.db('features')
.modify(FeatureToggleStore.filterByArchived, archived)
@ -148,14 +149,18 @@ export default class FeatureToggleClientStore
}
if (userId && this.flagResolver.isEnabled('favorites')) {
query = query.leftJoin(`favorite_features as ff`, function () {
this.on('ff.feature', 'features.name').andOnVal(
'ff.user_id',
'=',
userId,
);
query = query.leftJoin(`favorite_features`, function () {
this.on(
'favorite_features.feature',
'features.name',
).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.enabled = !!r.enabled;
feature.name = r.name;
feature.favorite = r.favorite != null;
feature.favorite = r.favorite;
feature.description = r.description;
feature.project = r.project;
feature.stale = r.stale;

View File

@ -33,6 +33,7 @@ import GroupStore from './group-store';
import PatStore from './pat-store';
import { PublicSignupTokenStore } from './public-signup-token-store';
import { FavoriteFeaturesStore } from './favorite-features-store';
import { FavoriteProjectsStore } from './favorite-projects-store';
export const createStores = (
config: IUnleashConfig,
@ -56,7 +57,12 @@ export const createStores = (
contextFieldStore: new ContextFieldStore(db, getLogger),
settingStore: new SettingStore(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),
tagTypeStore: new TagTypeStore(db, eventBus, getLogger),
addonStore: new AddonStore(db, eventBus, getLogger),
@ -100,6 +106,11 @@ export const createStores = (
eventBus,
getLogger,
),
favoriteProjectsStore: new FavoriteProjectsStore(
db,
eventBus,
getLogger,
),
};
};

View File

@ -13,6 +13,8 @@ import { DEFAULT_ENV } from '../util/constants';
import metricsHelper from '../util/metrics-helper';
import { DB_TIME } from '../metric-events';
import EventEmitter from 'events';
import { IFlagResolver } from '../types';
import Raw = Knex.Raw;
const COLUMNS = [
'id',
@ -39,9 +41,16 @@ class ProjectStore implements IProjectStore {
private logger: Logger;
private flagResolver: IFlagResolver;
private timer: Function;
constructor(db: Knex, eventBus: EventEmitter, getLogger: LogProvider) {
constructor(
db: Knex,
eventBus: EventEmitter,
getLogger: LogProvider,
flagResolver: IFlagResolver,
) {
this.db = db;
this.logger = getLogger('project-store.ts');
this.timer = (action) =>
@ -49,6 +58,7 @@ class ProjectStore implements IProjectStore {
store: 'project',
action,
});
this.flagResolver = flagResolver;
}
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
@ -73,21 +83,43 @@ class ProjectStore implements IProjectStore {
async getProjectsWithCounts(
query?: IProjectQuery,
userId?: number,
): 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;
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(
this.mapProjectWithCountRow,
@ -112,6 +144,7 @@ class ProjectStore implements IProjectStore {
id: row.id,
description: row.description,
health: row.health,
favorite: row.favorite,
featureCount: Number(row.number_of_features) || 0,
memberCount: Number(row.number_of_users) || 0,
updatedAt: row.updated_at,

View File

@ -45,6 +45,9 @@ export const healthOverviewSchema = {
format: 'date-time',
nullable: true,
},
favorite: {
type: 'boolean',
},
},
components: {
schemas: {

View File

@ -36,6 +36,9 @@ export const projectSchema = {
changeRequestsEnabled: {
type: 'boolean',
},
favorite: {
type: 'boolean',
},
},
components: {},
} as const;

View File

@ -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(
@ -79,4 +107,30 @@ export default class FavoritesController extends Controller {
});
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();
}
}

View File

@ -17,6 +17,7 @@ import {
healthReportSchema,
HealthReportSchema,
} from '../../../openapi/spec/health-report-schema';
import { IAuthRequest } from '../../unleash-types';
export default class ProjectHealthReport extends Controller {
private projectHealthService: ProjectHealthService;
@ -71,14 +72,16 @@ export default class ProjectHealthReport extends Controller {
}
async getProjectHealthOverview(
req: Request<IProjectParam, unknown, unknown, IArchivedQuery>,
req: IAuthRequest<IProjectParam, unknown, unknown, IArchivedQuery>,
res: Response<HealthOverviewSchema>,
): Promise<void> {
const { projectId } = req.params;
const { archived } = req.query;
const { user } = req;
const overview = await this.projectHealthService.getProjectOverview(
projectId,
archived,
user.id,
);
this.openApiService.respondWithValidation(
200,

View File

@ -1,4 +1,4 @@
import { Request, Response } from 'express';
import { Response } from 'express';
import Controller from '../../controller';
import { IUnleashConfig } from '../../../types/option';
import { IUnleashServices } from '../../../types/services';
@ -15,6 +15,7 @@ import {
import { OpenApiService } from '../../../services/openapi-service';
import { serializeDates } from '../../../types/serialize-dates';
import { createResponseSchema } from '../../../openapi/util/create-response-schema';
import { IAuthRequest } from '../../unleash-types';
export default class ProjectApi extends Controller {
private projectService: ProjectService;
@ -49,12 +50,16 @@ export default class ProjectApi extends Controller {
}
async getProjects(
req: Request,
req: IAuthRequest,
res: Response<ProjectsSchema>,
): Promise<void> {
const projects = await this.projectService.getProjects({
id: 'default',
});
const { user } = req;
const projects = await this.projectService.getProjects(
{
id: 'default',
},
user.id,
);
this.openApiService.respondWithValidation(
200,

View File

@ -5,7 +5,11 @@ import {
IFavoriteFeatureKey,
IFavoriteFeaturesStore,
} 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 {
private config: IUnleashConfig;
@ -14,15 +18,22 @@ export class FavoritesService {
private favoriteFeaturesStore: IFavoriteFeaturesStore;
private favoriteProjectsStore: IFavoriteProjectsStore;
constructor(
{
favoriteFeaturesStore,
}: Pick<IUnleashStores, 'favoriteFeaturesStore'>,
favoriteProjectsStore,
}: Pick<
IUnleashStores,
'favoriteFeaturesStore' | 'favoriteProjectsStore'
>,
config: IUnleashConfig,
) {
this.config = config;
this.logger = config.getLogger('services/favorites-service.ts');
this.favoriteFeaturesStore = favoriteFeaturesStore;
this.favoriteProjectsStore = favoriteProjectsStore;
}
async addFavoriteFeature(
@ -34,4 +45,27 @@ export class FavoritesService {
async removeFavoriteFeature(favorite: IFavoriteFeatureKey): Promise<void> {
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);
}
}

View File

@ -84,10 +84,12 @@ export const createServices = (
);
const environmentService = new EnvironmentService(stores, config);
const featureTagService = new FeatureTagService(stores, config);
const favoritesService = new FavoritesService(stores, config);
const projectHealthService = new ProjectHealthService(
stores,
config,
featureToggleServiceV2,
favoritesService,
);
const projectService = new ProjectService(
stores,
@ -124,7 +126,6 @@ export const createServices = (
config,
versionService,
);
const favoritesService = new FavoritesService(stores, config);
return {
accessService,

View File

@ -14,6 +14,7 @@ import { IProjectStore } from '../types/stores/project-store';
import FeatureToggleService from './feature-toggle-service';
import { hoursToMilliseconds } from 'date-fns';
import Timer = NodeJS.Timer;
import { FavoritesService } from './favorites-service';
export default class ProjectHealthService {
private logger: Logger;
@ -30,6 +31,8 @@ export default class ProjectHealthService {
private featureToggleService: FeatureToggleService;
private favoritesService: FavoritesService;
constructor(
{
projectStore,
@ -41,6 +44,7 @@ export default class ProjectHealthService {
>,
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>,
featureToggleService: FeatureToggleService,
favoritesService: FavoritesService,
) {
this.logger = getLogger('services/project-health-service.ts');
this.projectStore = projectStore;
@ -52,12 +56,14 @@ export default class ProjectHealthService {
hoursToMilliseconds(1),
).unref();
this.featureToggleService = featureToggleService;
this.favoritesService = favoritesService;
}
// TODO: duplicate from project-service.
async getProjectOverview(
projectId: string,
archived: boolean = false,
userId?: number,
): Promise<IProjectOverview> {
const project = await this.projectStore.get(projectId);
const environments = await this.projectStore.getEnvironmentsForProject(
@ -70,10 +76,16 @@ export default class ProjectHealthService {
const members = await this.projectStore.getMembersCountByProject(
projectId,
);
const favorite = await this.favoritesService.isFavoriteProject(
projectId,
userId,
);
return {
name: project.name,
description: project.description,
health: project.health,
favorite: favorite,
updatedAt: project.updatedAt,
environments,
features,
@ -85,7 +97,11 @@ export default class ProjectHealthService {
async getProjectHealthReport(
projectId: string,
): Promise<IProjectHealthReport> {
const overview = await this.getProjectOverview(projectId, false);
const overview = await this.getProjectOverview(
projectId,
false,
undefined,
);
return {
...overview,
potentiallyStaleCount: await this.potentiallyStaleCount(

View File

@ -120,8 +120,11 @@ export default class ProjectService {
this.logger = config.getLogger('services/project-service.js');
}
async getProjects(query?: IProjectQuery): Promise<IProjectWithCount[]> {
return this.store.getProjectsWithCounts(query);
async getProjects(
query?: IProjectQuery,
userId?: number,
): Promise<IProjectWithCount[]> {
return this.store.getProjectsWithCounts(query, userId);
}
async getProject(id: string): Promise<IProject> {

View File

@ -70,7 +70,6 @@ export interface IFeatureToggleClient {
lastSeenAt?: Date;
createdAt?: Date;
tags?: ITag[];
favorite?: boolean;
}
@ -176,6 +175,7 @@ export interface IProjectOverview {
members: number;
version: number;
health: number;
favorite?: boolean;
updatedAt?: Date;
}
@ -371,6 +371,7 @@ export interface ICustomRole {
export interface IProjectWithCount extends IProject {
featureCount: number;
memberCount: number;
favorite?: boolean;
}
export interface ISegment {

View File

@ -29,6 +29,7 @@ import { IGroupStore } from './stores/group-store';
import { IPatStore } from './stores/pat-store';
import { IPublicSignupTokenStore } from './stores/public-signup-token-store';
import { IFavoriteFeaturesStore } from './stores/favorite-features';
import { IFavoriteProjectsStore } from './stores/favorite-projects';
export interface IUnleashStores {
accessStore: IAccessStore;
@ -62,6 +63,7 @@ export interface IUnleashStores {
patStore: IPatStore;
publicSignupTokenStore: IPublicSignupTokenStore;
favoriteFeaturesStore: IFavoriteFeaturesStore;
favoriteProjectsStore: IFavoriteProjectsStore;
}
export {
@ -96,4 +98,5 @@ export {
IUserSplashStore,
IUserStore,
IFavoriteFeaturesStore,
IFavoriteProjectsStore,
};

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

View File

@ -34,21 +34,37 @@ export interface IProjectEnvironmentWithChangeRequests {
export interface IProjectStore extends Store<IProject, string> {
hasProject(id: string): Promise<boolean>;
updateHealth(healthUpdate: IProjectHealthUpdate): Promise<void>;
create(project: IProjectInsert): Promise<IProject>;
update(update: IProjectInsert): Promise<void>;
importProjects(
projects: IProjectInsert[],
environments?: IEnvironment[],
): Promise<IProject[]>;
addEnvironmentToProject(id: string, environment: string): Promise<void>;
deleteEnvironmentForProject(id: string, environment: string): Promise<void>;
getEnvironmentsForProject(id: string): Promise<string[]>;
getMembersCountByProject(projectId: string): Promise<number>;
getProjectsByUser(userId: number): Promise<string[]>;
getMembersCount(): Promise<IProjectMembersCount[]>;
getProjectsWithCounts(query?: IProjectQuery): Promise<IProjectWithCount[]>;
getProjectsWithCounts(
query?: IProjectQuery,
userId?: number,
): Promise<IProjectWithCount[]>;
count(): Promise<number>;
getAll(query?: IProjectQuery): Promise<IProject[]>;
getProjectLinksForEnvironments(

View File

@ -51,6 +51,34 @@ const unfavoriteFeature = async (featureName: string) => {
.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 () => {
db = await dbInit('favorites_api_serial', getLogger);
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 () => {
const featureName = 'test-feature';
await createFeature(featureName);
await favoriteFeature(featureName);
await unfavoriteFeature(featureName);
@ -162,3 +189,38 @@ test('should be able to unfavorite feature', async () => {
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,
});
});

View File

@ -1549,6 +1549,9 @@ exports[`should serve the OpenAPI spec 1`] = `
},
"type": "array",
},
"favorite": {
"type": "boolean",
},
"features": {
"items": {
"$ref": "#/components/schemas/featureSchema",
@ -1594,6 +1597,9 @@ exports[`should serve the OpenAPI spec 1`] = `
},
"type": "array",
},
"favorite": {
"type": "boolean",
},
"features": {
"items": {
"$ref": "#/components/schemas/featureSchema",
@ -2352,6 +2358,9 @@ exports[`should serve the OpenAPI spec 1`] = `
"description": {
"type": "string",
},
"favorite": {
"type": "boolean",
},
"featureCount": {
"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": {
"get": {
"operationId": "getFeatures",

View File

@ -9,6 +9,7 @@ import { IUnleashStores } from '../../../lib/types';
import { IUser } from '../../../lib/server-impl';
import { SegmentService } from '../../../lib/services/segment-service';
import { GroupService } from '../../../lib/services/group-service';
import { FavoritesService } from '../../../lib/services';
let stores: IUnleashStores;
let db: ITestDb;
@ -17,6 +18,7 @@ let groupService;
let accessService;
let projectHealthService;
let featureToggleService;
let favoritesService;
let user: IUser;
beforeAll(async () => {
@ -42,10 +44,12 @@ beforeAll(async () => {
featureToggleService,
groupService,
);
favoritesService = new FavoritesService(stores, config);
projectHealthService = new ProjectHealthService(
stores,
config,
featureToggleService,
favoritesService,
);
});

View 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([]);
}
}

View File

@ -30,6 +30,7 @@ import FakeGroupStore from './fake-group-store';
import FakePatStore from './fake-pat-store';
import FakePublicSignupStore from './fake-public-signup-store';
import FakeFavoriteFeaturesStore from './fake-favorite-features-store';
import FakeFavoriteProjectsStore from './fake-favorite-projects-store';
const createStores: () => IUnleashStores = () => {
const db = {
@ -71,6 +72,7 @@ const createStores: () => IUnleashStores = () => {
patStore: new FakePatStore(),
publicSignupTokenStore: new FakePublicSignupStore(),
favoriteFeaturesStore: new FakeFavoriteFeaturesStore(),
favoriteProjectsStore: new FakeFavoriteProjectsStore(),
};
};