mirror of
https://github.com/Unleash/unleash.git
synced 2025-02-23 00:22:19 +01:00
[Gitar] Cleaning up stale flag: useProjectReadModel with value true (#8211)
Co-authored-by: Gitar <noreply@gitar.co> Co-authored-by: Tymoteusz Czech <2625371+Tymek@users.noreply.github.com>
This commit is contained in:
parent
5dd0fb9f44
commit
338b5ce853
@ -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 SystemOwner = { ownerType: 'system' };
|
||||||
export type UserProjectOwner = {
|
export type UserProjectOwner = {
|
||||||
@ -17,13 +17,13 @@ type ProjectOwners =
|
|||||||
|
|
||||||
export type ProjectOwnersDictionary = Record<string, ProjectOwners>;
|
export type ProjectOwnersDictionary = Record<string, ProjectOwners>;
|
||||||
|
|
||||||
export type IProjectForUiWithOwners = TransitionalProjectData & {
|
export type IProjectForUiWithOwners = ProjectForUi & {
|
||||||
owners: ProjectOwners;
|
owners: ProjectOwners;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface IProjectOwnersReadModel {
|
export interface IProjectOwnersReadModel {
|
||||||
addOwners(
|
addOwners(
|
||||||
projects: TransitionalProjectData[],
|
projects: ProjectForUi[],
|
||||||
anonymizeProjectOwners?: boolean,
|
anonymizeProjectOwners?: boolean,
|
||||||
): Promise<IProjectForUiWithOwners[]>;
|
): Promise<IProjectForUiWithOwners[]>;
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import type { IProjectWithCount, ProjectMode } from '../../types';
|
import type { ProjectMode } from '../../types';
|
||||||
import type { IProjectQuery } from './project-store-type';
|
import type { IProjectQuery } from './project-store-type';
|
||||||
|
|
||||||
export type ProjectForUi = {
|
export type ProjectForUi = {
|
||||||
@ -16,9 +16,6 @@ export type ProjectForUi = {
|
|||||||
lastUpdatedAt: Date | null;
|
lastUpdatedAt: Date | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// @todo remove with flag useProjectReadModel
|
|
||||||
export type TransitionalProjectData = ProjectForUi | IProjectWithCount;
|
|
||||||
|
|
||||||
export type ProjectForInsights = {
|
export type ProjectForInsights = {
|
||||||
id: string;
|
id: string;
|
||||||
health: number;
|
health: number;
|
||||||
|
@ -82,9 +82,7 @@ beforeAll(async () => {
|
|||||||
await stores.accessStore.addUserToRole(opsUser.id, 1, '');
|
await stores.accessStore.addUserToRole(opsUser.id, 1, '');
|
||||||
const config = createTestConfig({
|
const config = createTestConfig({
|
||||||
getLogger,
|
getLogger,
|
||||||
experimental: {
|
experimental: {},
|
||||||
flags: { useProjectReadModel: true },
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
eventService = createEventsService(db.rawDatabase, config);
|
eventService = createEventsService(db.rawDatabase, config);
|
||||||
accessService = createAccessService(db.rawDatabase, config);
|
accessService = createAccessService(db.rawDatabase, config);
|
||||||
|
@ -88,7 +88,7 @@ import type { IProjectFlagCreatorsReadModel } from './project-flag-creators-read
|
|||||||
import { throwExceedsLimitError } from '../../error/exceeds-limit-error';
|
import { throwExceedsLimitError } from '../../error/exceeds-limit-error';
|
||||||
import type EventEmitter from 'events';
|
import type EventEmitter from 'events';
|
||||||
import type { ApiTokenService } from '../../services/api-token-service';
|
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';
|
import { canGrantProjectRole } from './can-grant-project-role';
|
||||||
|
|
||||||
type Days = number;
|
type Days = number;
|
||||||
@ -232,10 +232,9 @@ export default class ProjectService {
|
|||||||
async getProjects(
|
async getProjects(
|
||||||
query?: IProjectQuery,
|
query?: IProjectQuery,
|
||||||
userId?: number,
|
userId?: number,
|
||||||
): Promise<TransitionalProjectData[]> {
|
): Promise<ProjectForUi[]> {
|
||||||
const getProjects = this.flagResolver.isEnabled('useProjectReadModel')
|
const getProjects = () =>
|
||||||
? () => this.projectReadModel.getProjectsForAdminUi(query, userId)
|
this.projectReadModel.getProjectsForAdminUi(query, userId);
|
||||||
: () => this.projectStore.getProjectsWithCounts(query, userId);
|
|
||||||
|
|
||||||
const projects = await getProjects();
|
const projects = await getProjects();
|
||||||
|
|
||||||
@ -257,8 +256,8 @@ export default class ProjectService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async addOwnersToProjects(
|
async addOwnersToProjects(
|
||||||
projects: TransitionalProjectData[],
|
projects: ProjectForUi[],
|
||||||
): Promise<TransitionalProjectData[]> {
|
): Promise<ProjectForUi[]> {
|
||||||
const anonymizeProjectOwners = this.flagResolver.isEnabled(
|
const anonymizeProjectOwners = this.flagResolver.isEnabled(
|
||||||
'anonymizeProjectOwners',
|
'anonymizeProjectOwners',
|
||||||
);
|
);
|
||||||
|
@ -7,7 +7,6 @@ import type {
|
|||||||
IFeatureNaming,
|
IFeatureNaming,
|
||||||
IProject,
|
IProject,
|
||||||
IProjectApplications,
|
IProjectApplications,
|
||||||
IProjectWithCount,
|
|
||||||
ProjectMode,
|
ProjectMode,
|
||||||
} from '../../types/model';
|
} from '../../types/model';
|
||||||
import type { Store } from '../../types/stores/store';
|
import type { Store } from '../../types/stores/store';
|
||||||
@ -99,14 +98,6 @@ export interface IProjectStore extends Store<IProject, string> {
|
|||||||
|
|
||||||
getProjectsByUser(userId: number): Promise<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>;
|
count(): Promise<number>;
|
||||||
|
|
||||||
getAll(query?: IProjectQuery): Promise<IProject[]>;
|
getAll(query?: IProjectQuery): Promise<IProject[]>;
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { Knex } from 'knex';
|
|
||||||
import type { Logger, LogProvider } from '../../logger';
|
import type { Logger, LogProvider } from '../../logger';
|
||||||
|
|
||||||
import NotFoundError from '../../error/notfound-error';
|
import NotFoundError from '../../error/notfound-error';
|
||||||
@ -9,7 +8,6 @@ import type {
|
|||||||
IProjectApplication,
|
IProjectApplication,
|
||||||
IProjectApplications,
|
IProjectApplications,
|
||||||
IProjectUpdate,
|
IProjectUpdate,
|
||||||
IProjectWithCount,
|
|
||||||
ProjectMode,
|
ProjectMode,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
import type {
|
import type {
|
||||||
@ -27,7 +25,6 @@ import metricsHelper from '../../util/metrics-helper';
|
|||||||
import { DB_TIME } from '../../metric-events';
|
import { DB_TIME } from '../../metric-events';
|
||||||
import type EventEmitter from 'events';
|
import type EventEmitter from 'events';
|
||||||
import type { Db } from '../../db/db';
|
import type { Db } from '../../db/db';
|
||||||
import Raw = Knex.Raw;
|
|
||||||
import type { CreateFeatureStrategySchema } from '../../openapi';
|
import type { CreateFeatureStrategySchema } from '../../openapi';
|
||||||
import { applySearchFilters } from '../feature-search/search-utils';
|
import { applySearchFilters } from '../feature-search/search-utils';
|
||||||
|
|
||||||
@ -117,114 +114,6 @@ class ProjectStore implements IProjectStore {
|
|||||||
return present;
|
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 (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',
|
|
||||||
'projects.archived_at',
|
|
||||||
] as (string | Raw<any>)[];
|
|
||||||
|
|
||||||
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[]> {
|
async getAll(query: IProjectQuery = {}): Promise<IProject[]> {
|
||||||
let projects = this.db
|
let projects = this.db
|
||||||
.select(COLUMNS)
|
.select(COLUMNS)
|
||||||
|
@ -56,7 +56,6 @@ export type IFlagKey =
|
|||||||
| 'extendedMetrics'
|
| 'extendedMetrics'
|
||||||
| 'removeUnsafeInlineStyleSrc'
|
| 'removeUnsafeInlineStyleSrc'
|
||||||
| 'originMiddleware'
|
| 'originMiddleware'
|
||||||
| 'useProjectReadModel'
|
|
||||||
| 'addonUsageMetrics'
|
| 'addonUsageMetrics'
|
||||||
| 'onboardingMetrics'
|
| 'onboardingMetrics'
|
||||||
| 'onboardingUI'
|
| 'onboardingUI'
|
||||||
@ -279,10 +278,6 @@ const flags: IFlags = {
|
|||||||
process.env.UNLEASH_EXPERIMENTAL_ORIGIN_MIDDLEWARE,
|
process.env.UNLEASH_EXPERIMENTAL_ORIGIN_MIDDLEWARE,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
useProjectReadModel: parseEnvVarBoolean(
|
|
||||||
process.env.UNLEASH_EXPERIMENTAL_USE_PROJECT_READ_MODEL,
|
|
||||||
false,
|
|
||||||
),
|
|
||||||
addonUsageMetrics: parseEnvVarBoolean(
|
addonUsageMetrics: parseEnvVarBoolean(
|
||||||
process.env.UNLEASH_EXPERIMENTAL_ADDON_USAGE_METRICS,
|
process.env.UNLEASH_EXPERIMENTAL_ADDON_USAGE_METRICS,
|
||||||
false,
|
false,
|
||||||
|
@ -574,17 +574,6 @@ export interface ICustomRole extends IRole {
|
|||||||
description: string;
|
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 {
|
export interface IClientSegment {
|
||||||
id: number;
|
id: number;
|
||||||
constraints: IConstraint[];
|
constraints: IConstraint[];
|
||||||
|
@ -51,7 +51,6 @@ process.nextTick(async () => {
|
|||||||
enableLegacyVariants: false,
|
enableLegacyVariants: false,
|
||||||
extendedMetrics: true,
|
extendedMetrics: true,
|
||||||
originMiddleware: true,
|
originMiddleware: true,
|
||||||
useProjectReadModel: true,
|
|
||||||
addonUsageMetrics: true,
|
addonUsageMetrics: true,
|
||||||
onboardingMetrics: true,
|
onboardingMetrics: true,
|
||||||
onboardingUI: true,
|
onboardingUI: true,
|
||||||
|
23
src/test/fixtures/fake-project-store.ts
vendored
23
src/test/fixtures/fake-project-store.ts
vendored
@ -3,7 +3,6 @@ import type {
|
|||||||
IProject,
|
IProject,
|
||||||
IProjectApplications,
|
IProjectApplications,
|
||||||
IProjectStore,
|
IProjectStore,
|
||||||
IProjectWithCount,
|
|
||||||
} from '../../lib/types';
|
} from '../../lib/types';
|
||||||
import NotFoundError from '../../lib/error/notfound-error';
|
import NotFoundError from '../../lib/error/notfound-error';
|
||||||
import type {
|
import type {
|
||||||
@ -15,7 +14,6 @@ import type {
|
|||||||
IProjectApplicationsSearchParams,
|
IProjectApplicationsSearchParams,
|
||||||
IProjectHealthUpdate,
|
IProjectHealthUpdate,
|
||||||
IProjectInsert,
|
IProjectInsert,
|
||||||
IProjectQuery,
|
|
||||||
ProjectEnvironment,
|
ProjectEnvironment,
|
||||||
} from '../../lib/features/project/project-store-type';
|
} from '../../lib/features/project/project-store-type';
|
||||||
|
|
||||||
@ -48,27 +46,6 @@ export default class FakeProjectStore implements IProjectStore {
|
|||||||
this.projectEnvironment.set(id, environments);
|
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 {
|
private createInternal(project: IProjectInsert): IProject {
|
||||||
const newProj: ArchivableProject = {
|
const newProj: ArchivableProject = {
|
||||||
...project,
|
...project,
|
||||||
|
Loading…
Reference in New Issue
Block a user