Skip to content

Commit

Permalink
Favorite project (#2569)
Browse files Browse the repository at this point in the history
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`
  • Loading branch information
sjaanus committed Nov 30, 2022
1 parent fab6fbb commit a22d5f5
Show file tree
Hide file tree
Showing 23 changed files with 507 additions and 42 deletions.
96 changes: 96 additions & 0 deletions src/lib/db/favorite-projects-store.ts
@@ -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);
}
}
26 changes: 17 additions & 9 deletions src/lib/db/feature-strategy-store.ts
Expand Up @@ -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',
Expand Down Expand Up @@ -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(
Expand All @@ -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);
Expand All @@ -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;
Expand Down Expand Up @@ -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(
Expand All @@ -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);
Expand All @@ -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,
Expand Down
23 changes: 14 additions & 9 deletions src/lib/db/feature-toggle-client-store.ts
Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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',
),
];
}
}

Expand Down Expand Up @@ -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;
Expand Down
13 changes: 12 additions & 1 deletion src/lib/db/index.ts
Expand Up @@ -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,
Expand All @@ -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),
Expand Down Expand Up @@ -100,6 +106,11 @@ export const createStores = (
eventBus,
getLogger,
),
favoriteProjectsStore: new FavoriteProjectsStore(
db,
eventBus,
getLogger,
),
};
};

Expand Down
49 changes: 41 additions & 8 deletions src/lib/db/project-store.ts
Expand Up @@ -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',
Expand All @@ -39,16 +41,24 @@ 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) =>
metricsHelper.wrapTimer(eventBus, DB_TIME, {
store: 'project',
action,
});
this.flagResolver = flagResolver;
}

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
Expand All @@ -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,
Expand All @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions src/lib/openapi/spec/health-overview-schema.ts
Expand Up @@ -45,6 +45,9 @@ export const healthOverviewSchema = {
format: 'date-time',
nullable: true,
},
favorite: {
type: 'boolean',
},
},
components: {
schemas: {
Expand Down
3 changes: 3 additions & 0 deletions src/lib/openapi/spec/project-schema.ts
Expand Up @@ -36,6 +36,9 @@ export const projectSchema = {
changeRequestsEnabled: {
type: 'boolean',
},
favorite: {
type: 'boolean',
},
},
components: {},
} as const;
Expand Down

0 comments on commit a22d5f5

Please sign in to comment.