Skip to content

Commit

Permalink
fix: project with count cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
Tymek committed Sep 23, 2024
1 parent 7157717 commit 305d1d3
Showing 7 changed files with 8 additions and 170 deletions.
6 changes: 3 additions & 3 deletions src/lib/features/project/project-owners-read-model.type.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { TransitionalProjectData } from './project-read-model-type';
import type { ProjectForUi } from './project-read-model-type';

export type SystemOwner = { ownerType: 'system' };
export type UserProjectOwner = {
@@ -17,13 +17,13 @@ type ProjectOwners =

export type ProjectOwnersDictionary = Record<string, ProjectOwners>;

export type IProjectForUiWithOwners = TransitionalProjectData & {
export type IProjectForUiWithOwners = ProjectForUi & {
owners: ProjectOwners;
};

export interface IProjectOwnersReadModel {
addOwners(
projects: TransitionalProjectData[],
projects: ProjectForUi[],
anonymizeProjectOwners?: boolean,
): Promise<IProjectForUiWithOwners[]>;
}
5 changes: 1 addition & 4 deletions src/lib/features/project/project-read-model-type.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { IProjectWithCount, ProjectMode } from '../../types';
import type { ProjectMode } from '../../types';
import type { IProjectQuery } from './project-store-type';

export type ProjectForUi = {
@@ -16,9 +16,6 @@ export type ProjectForUi = {
lastUpdatedAt: Date | null;
};

// @todo remove with flag useProjectReadModel
export type TransitionalProjectData = ProjectForUi | IProjectWithCount;

export type ProjectForInsights = {
id: string;
health: number;
8 changes: 4 additions & 4 deletions src/lib/features/project/project-service.ts
Original file line number Diff line number Diff line change
@@ -88,7 +88,7 @@ import type { IProjectFlagCreatorsReadModel } from './project-flag-creators-read
import { throwExceedsLimitError } from '../../error/exceeds-limit-error';
import type EventEmitter from 'events';
import type { ApiTokenService } from '../../services/api-token-service';
import type { TransitionalProjectData } from './project-read-model-type';
import type { ProjectForUi } from './project-read-model-type';
import { canGrantProjectRole } from './can-grant-project-role';

type Days = number;
@@ -232,7 +232,7 @@ export default class ProjectService {
async getProjects(
query?: IProjectQuery,
userId?: number,
): Promise<TransitionalProjectData[]> {
): Promise<ProjectForUi[]> {
const getProjects = () =>
this.projectReadModel.getProjectsForAdminUi(query, userId);

@@ -256,8 +256,8 @@ export default class ProjectService {
}

async addOwnersToProjects(
projects: TransitionalProjectData[],
): Promise<TransitionalProjectData[]> {
projects: ProjectForUi[],
): Promise<ProjectForUi[]> {
const anonymizeProjectOwners = this.flagResolver.isEnabled(
'anonymizeProjectOwners',
);
9 changes: 0 additions & 9 deletions src/lib/features/project/project-store-type.ts
Original file line number Diff line number Diff line change
@@ -7,7 +7,6 @@ import type {
IFeatureNaming,
IProject,
IProjectApplications,
IProjectWithCount,
ProjectMode,
} from '../../types/model';
import type { Store } from '../../types/stores/store';
@@ -99,14 +98,6 @@ export interface IProjectStore extends Store<IProject, string> {

getProjectsByUser(userId: number): Promise<string[]>;

/**
* @deprecated Use the appropriate method in the project read model instead.
*/
getProjectsWithCounts(
query?: IProjectQuery,
userId?: number,
): Promise<IProjectWithCount[]>;

count(): Promise<number>;

getAll(query?: IProjectQuery): Promise<IProject[]>;
116 changes: 0 additions & 116 deletions src/lib/features/project/project-store.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Knex } from 'knex';
import type { Logger, LogProvider } from '../../logger';

import NotFoundError from '../../error/notfound-error';
@@ -9,7 +8,6 @@ import type {
IProjectApplication,
IProjectApplications,
IProjectUpdate,
IProjectWithCount,
ProjectMode,
} from '../../types';
import type {
@@ -27,7 +25,6 @@ import metricsHelper from '../../util/metrics-helper';
import { DB_TIME } from '../../metric-events';
import type EventEmitter from 'events';
import type { Db } from '../../db/db';
import Raw = Knex.Raw;
import type { CreateFeatureStrategySchema } from '../../openapi';
import { applySearchFilters } from '../feature-search/search-utils';

@@ -117,119 +114,6 @@ class ProjectStore implements IProjectStore {
return present;
}

async getProjectsWithCounts(
query?: IProjectQuery,
userId?: number,
): Promise<IProjectWithCount[]> {
const projectTimer = this.timer('getProjectsWithCount');
let projects = this.db(TABLE)
.leftJoin('features', 'features.project', 'projects.id')
.leftJoin(
'project_settings',
'project_settings.project',
'projects.id',
)
.leftJoin('project_stats', 'project_stats.project', 'projects.id')
.orderBy('projects.name', 'asc');

if (this.flagResolver.isEnabled('archiveProjects')) {
if (query?.archived === true) {
projects = projects.whereNot(`${TABLE}.archived_at`, null);
} else {
projects = projects.where(`${TABLE}.archived_at`, null);
}
}

if (query?.id) {
projects = projects.where(`${TABLE}.id`, query.id);
}

let selectColumns = [
this.db.raw(
'projects.id, projects.name, projects.description, projects.health, projects.updated_at, projects.created_at, ' +
'count(features.name) FILTER (WHERE features.archived_at is null) AS number_of_features, ' +
'count(features.name) FILTER (WHERE features.archived_at is null and features.stale IS TRUE) AS stale_feature_count, ' +
'count(features.name) FILTER (WHERE features.archived_at is null and features.potentially_stale IS TRUE) AS potentially_stale_feature_count',
),
'project_settings.default_stickiness',
'project_settings.project_mode',
'project_stats.avg_time_to_prod_current_window',
] as (string | Raw<any>)[];

if (this.flagResolver.isEnabled('archiveProjects')) {
selectColumns.push(`${TABLE}.archived_at`);
}

let groupByColumns = [
'projects.id',
'project_settings.default_stickiness',
'project_settings.project_mode',
'project_stats.avg_time_to_prod_current_window',
];

if (userId) {
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,
);
projectTimer();
const memberTimer = this.timer('getMemberCount');

const memberCount = await this.getMembersCount();
memberTimer();
const memberMap = new Map<string, number>(
memberCount.map((c) => [c.project, Number(c.count)]),
);

return projectsWithFeatureCount.map((projectWithCount) => {
return {
...projectWithCount,
memberCount: memberMap.get(projectWithCount.id) || 0,
};
});
}

// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
mapProjectWithCountRow(row): IProjectWithCount {
return {
name: row.name,
id: row.id,
description: row.description,
health: row.health,
favorite: row.favorite,
featureCount: Number(row.number_of_features) || 0,
staleFeatureCount: Number(row.stale_feature_count) || 0,
potentiallyStaleFeatureCount:
Number(row.potentially_stale_feature_count) || 0,
memberCount: Number(row.number_of_users) || 0,
updatedAt: row.updated_at,
createdAt: row.created_at,
archivedAt: row.archived_at,
mode: row.project_mode || 'open',
defaultStickiness: row.default_stickiness || 'default',
avgTimeToProduction: row.avg_time_to_prod_current_window || 0,
};
}

async getAll(query: IProjectQuery = {}): Promise<IProject[]> {
let projects = this.db
.select(COLUMNS)
11 changes: 0 additions & 11 deletions src/lib/types/model.ts
Original file line number Diff line number Diff line change
@@ -574,17 +574,6 @@ export interface ICustomRole extends IRole {
description: string;
}

// @deprecated Remove with flag useProjectReadModel
export interface IProjectWithCount extends IProject {
featureCount: number;
staleFeatureCount: number;
potentiallyStaleFeatureCount: number;
memberCount: number;
favorite?: boolean;
avgTimeToProduction: number;
archivedAt?: Date;
}

export interface IClientSegment {
id: number;
constraints: IConstraint[];
23 changes: 0 additions & 23 deletions src/test/fixtures/fake-project-store.ts
Original file line number Diff line number Diff line change
@@ -3,7 +3,6 @@ import type {
IProject,
IProjectApplications,
IProjectStore,
IProjectWithCount,
} from '../../lib/types';
import NotFoundError from '../../lib/error/notfound-error';
import type {
@@ -15,7 +14,6 @@ import type {
IProjectApplicationsSearchParams,
IProjectHealthUpdate,
IProjectInsert,
IProjectQuery,
ProjectEnvironment,
} from '../../lib/features/project/project-store-type';

@@ -48,27 +46,6 @@ export default class FakeProjectStore implements IProjectStore {
this.projectEnvironment.set(id, environments);
}

async getProjectsWithCounts(
query?: IProjectQuery,
): Promise<IProjectWithCount[]> {
return this.projects
.filter((project) =>
query?.archived
? project.archivedAt !== null
: project.archivedAt === null,
)
.map((project) => {
return {
...project,
memberCount: 0,
featureCount: 0,
staleFeatureCount: 0,
potentiallyStaleFeatureCount: 0,
avgTimeToProduction: 0,
};
});
}

private createInternal(project: IProjectInsert): IProject {
const newProj: ArchivableProject = {
...project,

0 comments on commit 305d1d3

Please sign in to comment.