mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-26 13:48:33 +02:00
refactor: move release plan stores to OSS (#9747)
This commit is contained in:
parent
e9ec1db3b7
commit
b2471633b4
@ -168,6 +168,7 @@
|
|||||||
"stoppable": "^1.1.0",
|
"stoppable": "^1.1.0",
|
||||||
"ts-toolbelt": "^9.6.0",
|
"ts-toolbelt": "^9.6.0",
|
||||||
"type-is": "^1.6.18",
|
"type-is": "^1.6.18",
|
||||||
|
"ulidx": "^2.4.1",
|
||||||
"unleash-client": "^6.6.0",
|
"unleash-client": "^6.6.0",
|
||||||
"uuid": "^9.0.0"
|
"uuid": "^9.0.0"
|
||||||
},
|
},
|
||||||
|
@ -1,4 +1,11 @@
|
|||||||
import type { IUnleashConfig, IUnleashStores } from '../types';
|
import {
|
||||||
|
type IUnleashConfig,
|
||||||
|
type IUnleashStores,
|
||||||
|
ReleasePlanMilestoneStore,
|
||||||
|
ReleasePlanMilestoneStrategyStore,
|
||||||
|
ReleasePlanStore,
|
||||||
|
ReleasePlanTemplateStore,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
import EventStore from '../features/events/event-store';
|
import EventStore from '../features/events/event-store';
|
||||||
import FeatureToggleStore from '../features/feature-toggle/feature-toggle-store';
|
import FeatureToggleStore from '../features/feature-toggle/feature-toggle-store';
|
||||||
@ -191,6 +198,11 @@ export const createStores = (
|
|||||||
uniqueConnectionReadModel: new UniqueConnectionReadModel(
|
uniqueConnectionReadModel: new UniqueConnectionReadModel(
|
||||||
new UniqueConnectionStore(db),
|
new UniqueConnectionStore(db),
|
||||||
),
|
),
|
||||||
|
releasePlanStore: new ReleasePlanStore(db, config),
|
||||||
|
releasePlanTemplateStore: new ReleasePlanTemplateStore(db, config),
|
||||||
|
releasePlanMilestoneStore: new ReleasePlanMilestoneStore(db, config),
|
||||||
|
releasePlanMilestoneStrategyStore:
|
||||||
|
new ReleasePlanMilestoneStrategyStore(db, config),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1426,32 +1426,50 @@ test('should return change request ids per environment', async () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const createReleasePlan = async ({
|
const createReleasePlan = async (
|
||||||
feature,
|
{
|
||||||
environment,
|
feature,
|
||||||
planId,
|
environment,
|
||||||
}: { feature: string; environment: string; planId: string }) => {
|
planId,
|
||||||
await db.rawDatabase('release_plan_definitions').insert({
|
}: { feature: string; environment: string; planId: string },
|
||||||
id: planId,
|
milestones: {
|
||||||
discriminator: 'plan',
|
name: string;
|
||||||
|
order: number;
|
||||||
|
}[],
|
||||||
|
) => {
|
||||||
|
const result = await db.stores.releasePlanTemplateStore.insert({
|
||||||
name: 'plan',
|
name: 'plan',
|
||||||
feature_name: feature,
|
createdByUserId: 1,
|
||||||
environment: environment,
|
discriminator: 'template',
|
||||||
created_by_user_id: 1,
|
|
||||||
});
|
});
|
||||||
|
const releasePlan = await db.stores.releasePlanStore.insert({
|
||||||
|
id: planId,
|
||||||
|
name: 'plan',
|
||||||
|
featureName: feature,
|
||||||
|
environment: environment,
|
||||||
|
createdByUserId: 1,
|
||||||
|
releasePlanTemplateId: result.id,
|
||||||
|
});
|
||||||
|
const milestoneResults = await Promise.all(
|
||||||
|
milestones.map((milestone) =>
|
||||||
|
createMilestone({
|
||||||
|
...milestone,
|
||||||
|
planId: releasePlan.id,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return { releasePlan, milestones: milestoneResults };
|
||||||
};
|
};
|
||||||
|
|
||||||
const createMilestone = async ({
|
const createMilestone = async ({
|
||||||
id,
|
|
||||||
name,
|
name,
|
||||||
order,
|
order,
|
||||||
planId,
|
planId,
|
||||||
}: { id: string; name: string; order: number; planId: string }) => {
|
}: { name: string; order: number; planId: string }) => {
|
||||||
await db.rawDatabase('milestones').insert({
|
return db.stores.releasePlanMilestoneStore.insert({
|
||||||
id,
|
|
||||||
name,
|
name,
|
||||||
sort_order: order,
|
sortOrder: order,
|
||||||
release_plan_definition_id: planId,
|
releasePlanDefinitionId: planId,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1459,39 +1477,39 @@ const activateMilestone = async ({
|
|||||||
planId,
|
planId,
|
||||||
milestoneId,
|
milestoneId,
|
||||||
}: { planId: string; milestoneId: string }) => {
|
}: { planId: string; milestoneId: string }) => {
|
||||||
await db
|
await db.stores.releasePlanStore.update(planId, {
|
||||||
.rawDatabase('release_plan_definitions')
|
activeMilestoneId: milestoneId,
|
||||||
.update({ active_milestone_id: milestoneId })
|
});
|
||||||
.where('id', planId);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
test('should return release plan milestones', async () => {
|
test('should return release plan milestones', async () => {
|
||||||
await app.createFeature('my_feature_a');
|
await app.createFeature('my_feature_a');
|
||||||
|
|
||||||
await createReleasePlan({
|
const { releasePlan, milestones } = await createReleasePlan(
|
||||||
feature: 'my_feature_a',
|
{
|
||||||
environment: 'development',
|
feature: 'my_feature_a',
|
||||||
planId: 'plan0',
|
environment: 'development',
|
||||||
|
planId: 'plan0',
|
||||||
|
},
|
||||||
|
[
|
||||||
|
{
|
||||||
|
name: 'Milestone 1',
|
||||||
|
order: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Milestone 2',
|
||||||
|
order: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Milestone 3',
|
||||||
|
order: 2,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
);
|
||||||
|
await activateMilestone({
|
||||||
|
planId: releasePlan.id,
|
||||||
|
milestoneId: milestones[1].id,
|
||||||
});
|
});
|
||||||
await createMilestone({
|
|
||||||
id: 'milestone0',
|
|
||||||
name: 'Milestone 1',
|
|
||||||
order: 0,
|
|
||||||
planId: 'plan0',
|
|
||||||
});
|
|
||||||
await createMilestone({
|
|
||||||
id: 'milestone1',
|
|
||||||
name: 'Milestone 2',
|
|
||||||
order: 1,
|
|
||||||
planId: 'plan0',
|
|
||||||
});
|
|
||||||
await createMilestone({
|
|
||||||
id: 'milestone3',
|
|
||||||
name: 'Milestone 3',
|
|
||||||
order: 2,
|
|
||||||
planId: 'plan0',
|
|
||||||
});
|
|
||||||
await activateMilestone({ planId: 'plan0', milestoneId: 'milestone1' });
|
|
||||||
|
|
||||||
const { body } = await searchFeatures({});
|
const { body } = await searchFeatures({});
|
||||||
|
|
||||||
|
@ -0,0 +1,48 @@
|
|||||||
|
import { ulid } from 'ulidx';
|
||||||
|
import type { ReleasePlanMilestone } from './release-plan-milestone';
|
||||||
|
import { CRUDStore, type CrudStoreConfig } from '../../db/crud/crud-store';
|
||||||
|
import type { Row } from '../../db/crud/row-type';
|
||||||
|
import type { Db } from '../../db/db';
|
||||||
|
|
||||||
|
const TABLE = 'milestones';
|
||||||
|
|
||||||
|
const fromRow = (row: any): ReleasePlanMilestone => {
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
sortOrder: row.sort_order,
|
||||||
|
releasePlanDefinitionId: row.release_plan_definition_id,
|
||||||
|
strategies: [],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ReleasePlanMilestoneWriteModel = Omit<ReleasePlanMilestone, 'id'>;
|
||||||
|
|
||||||
|
export class ReleasePlanMilestoneStore extends CRUDStore<
|
||||||
|
ReleasePlanMilestone,
|
||||||
|
ReleasePlanMilestoneWriteModel,
|
||||||
|
Row<ReleasePlanMilestone>,
|
||||||
|
ReleasePlanMilestone,
|
||||||
|
string
|
||||||
|
> {
|
||||||
|
constructor(db: Db, config: CrudStoreConfig) {
|
||||||
|
super(TABLE, db, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
override async insert(
|
||||||
|
item: ReleasePlanMilestoneWriteModel,
|
||||||
|
): Promise<ReleasePlanMilestone> {
|
||||||
|
const row = this.toRow(item);
|
||||||
|
row.id = ulid();
|
||||||
|
await this.db(TABLE).insert(row);
|
||||||
|
return fromRow(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteAllConnectedToReleasePlanTemplate(
|
||||||
|
templateId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
await this.db(TABLE)
|
||||||
|
.where('release_plan_definition_id', templateId)
|
||||||
|
.delete();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,118 @@
|
|||||||
|
import { ulid } from 'ulidx';
|
||||||
|
import type { ReleasePlanMilestoneStrategy } from './release-plan-milestone-strategy';
|
||||||
|
import { CRUDStore, type CrudStoreConfig } from '../../db/crud/crud-store';
|
||||||
|
import type { Row } from '../../db/crud/row-type';
|
||||||
|
import type { Db } from '../../db/db';
|
||||||
|
|
||||||
|
const TABLE = 'milestone_strategies';
|
||||||
|
|
||||||
|
export type ReleasePlanMilestoneStrategyWriteModel = Omit<
|
||||||
|
ReleasePlanMilestoneStrategy,
|
||||||
|
'id'
|
||||||
|
>;
|
||||||
|
|
||||||
|
const fromRow = (row: any): ReleasePlanMilestoneStrategy => {
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
milestoneId: row.milestone_id,
|
||||||
|
sortOrder: row.sort_order,
|
||||||
|
title: row.title,
|
||||||
|
strategyName: row.strategy_name,
|
||||||
|
parameters: row.parameters,
|
||||||
|
constraints: JSON.parse(row.constraints),
|
||||||
|
variants: JSON.parse(row.variants),
|
||||||
|
segments: [],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const toRow = (item: ReleasePlanMilestoneStrategyWriteModel) => {
|
||||||
|
return {
|
||||||
|
id: ulid(),
|
||||||
|
milestone_id: item.milestoneId,
|
||||||
|
sort_order: item.sortOrder,
|
||||||
|
title: item.title,
|
||||||
|
strategy_name: item.strategyName,
|
||||||
|
parameters: item.parameters ?? {},
|
||||||
|
constraints: JSON.stringify(item.constraints ?? []),
|
||||||
|
variants: JSON.stringify(item.variants ?? []),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const toUpdateRow = (item: ReleasePlanMilestoneStrategyWriteModel) => {
|
||||||
|
return {
|
||||||
|
milestone_id: item.milestoneId,
|
||||||
|
sort_order: item.sortOrder,
|
||||||
|
title: item.title,
|
||||||
|
strategy_name: item.strategyName,
|
||||||
|
parameters: item.parameters ?? {},
|
||||||
|
constraints: JSON.stringify(item.constraints ?? []),
|
||||||
|
variants: JSON.stringify(item.variants ?? []),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ReleasePlanMilestoneStrategyStore extends CRUDStore<
|
||||||
|
ReleasePlanMilestoneStrategy,
|
||||||
|
ReleasePlanMilestoneStrategyWriteModel,
|
||||||
|
Row<ReleasePlanMilestoneStrategy>,
|
||||||
|
ReleasePlanMilestoneStrategy,
|
||||||
|
string
|
||||||
|
> {
|
||||||
|
constructor(db: Db, config: CrudStoreConfig) {
|
||||||
|
super(TABLE, db, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
override async insert({
|
||||||
|
segments,
|
||||||
|
...strategy
|
||||||
|
}: ReleasePlanMilestoneStrategyWriteModel): Promise<ReleasePlanMilestoneStrategy> {
|
||||||
|
const row = toRow(strategy);
|
||||||
|
await this.db(TABLE).insert(row);
|
||||||
|
segments?.forEach(async (segmentId) => {
|
||||||
|
const segmentRow = {
|
||||||
|
milestone_strategy_id: row.id,
|
||||||
|
segment_id: segmentId,
|
||||||
|
};
|
||||||
|
await this.db('milestone_strategy_segments').insert(segmentRow);
|
||||||
|
});
|
||||||
|
return fromRow(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async updateStrategy(
|
||||||
|
strategyId: string,
|
||||||
|
{ segments, ...strategy }: ReleasePlanMilestoneStrategyWriteModel,
|
||||||
|
): Promise<ReleasePlanMilestoneStrategy> {
|
||||||
|
const rows = await this.db(this.tableName)
|
||||||
|
.where({ id: strategyId })
|
||||||
|
.update(toUpdateRow(strategy))
|
||||||
|
.returning('*');
|
||||||
|
return this.fromRow(rows[0]) as ReleasePlanMilestoneStrategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
async upsert(
|
||||||
|
strategyId: string,
|
||||||
|
{ segments, ...strategy }: ReleasePlanMilestoneStrategyWriteModel,
|
||||||
|
): Promise<ReleasePlanMilestoneStrategy> {
|
||||||
|
const releasePlanMilestoneStrategy = await this.updateStrategy(
|
||||||
|
strategyId,
|
||||||
|
strategy,
|
||||||
|
);
|
||||||
|
// now delete
|
||||||
|
await this.db('milestone_strategy_segments')
|
||||||
|
.where('milestone_strategy_id', strategyId)
|
||||||
|
.delete();
|
||||||
|
for (const segmentId of segments ?? []) {
|
||||||
|
const segmentRow = {
|
||||||
|
milestone_strategy_id: strategyId,
|
||||||
|
segment_id: segmentId,
|
||||||
|
};
|
||||||
|
await this.db('milestone_strategy_segments').insert(segmentRow);
|
||||||
|
}
|
||||||
|
return releasePlanMilestoneStrategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteStrategiesForMilestone(milestoneId: string): Promise<void> {
|
||||||
|
await this.db('milestone_strategies')
|
||||||
|
.where('milestone_id', milestoneId)
|
||||||
|
.delete();
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
import type { IFeatureStrategy } from '../../types';
|
||||||
|
|
||||||
|
export interface ReleasePlanMilestoneStrategy
|
||||||
|
extends Partial<
|
||||||
|
Pick<
|
||||||
|
IFeatureStrategy,
|
||||||
|
'title' | 'parameters' | 'constraints' | 'variants' | 'segments'
|
||||||
|
>
|
||||||
|
> {
|
||||||
|
id: string;
|
||||||
|
milestoneId: string;
|
||||||
|
sortOrder: number;
|
||||||
|
strategyName: string;
|
||||||
|
}
|
9
src/lib/features/release-plans/release-plan-milestone.ts
Normal file
9
src/lib/features/release-plans/release-plan-milestone.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import type { ReleasePlanMilestoneStrategy } from './release-plan-milestone-strategy';
|
||||||
|
|
||||||
|
export interface ReleasePlanMilestone {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
sortOrder: number;
|
||||||
|
releasePlanDefinitionId: string;
|
||||||
|
strategies?: ReleasePlanMilestoneStrategy[];
|
||||||
|
}
|
369
src/lib/features/release-plans/release-plan-store.ts
Normal file
369
src/lib/features/release-plans/release-plan-store.ts
Normal file
@ -0,0 +1,369 @@
|
|||||||
|
import type { ReleasePlan } from './release-plan';
|
||||||
|
import type { ReleasePlanMilestoneStrategy } from './release-plan-milestone-strategy';
|
||||||
|
import { CRUDStore, type CrudStoreConfig } from '../../db/crud/crud-store';
|
||||||
|
import type { Row } from '../../db/crud/row-type';
|
||||||
|
import type { Db } from '../../db/db';
|
||||||
|
import { defaultToRow } from '../../db/crud/default-mappings';
|
||||||
|
import type { IAuditUser } from '../../types';
|
||||||
|
|
||||||
|
const TABLE = 'release_plan_definitions';
|
||||||
|
|
||||||
|
type ReleasePlanWriteModel = Omit<
|
||||||
|
ReleasePlan,
|
||||||
|
'discriminator' | 'createdAt' | 'milestones'
|
||||||
|
>;
|
||||||
|
|
||||||
|
const selectColumns = [
|
||||||
|
'rpd.id AS planId',
|
||||||
|
'rpd.discriminator AS planDiscriminator',
|
||||||
|
'rpd.name AS planName',
|
||||||
|
'rpd.description as planDescription',
|
||||||
|
'rpd.feature_name as planFeatureName',
|
||||||
|
'rpd.environment as planEnvironment',
|
||||||
|
'rpd.created_by_user_id as planCreatedByUserId',
|
||||||
|
'rpd.created_at as planCreatedAt',
|
||||||
|
'rpd.active_milestone_id as planActiveMilestoneId',
|
||||||
|
'rpd.release_plan_template_id as planTemplateId',
|
||||||
|
'mi.id AS milestoneId',
|
||||||
|
'mi.name AS milestoneName',
|
||||||
|
'mi.sort_order AS milestoneSortOrder',
|
||||||
|
'ms.id AS strategyId',
|
||||||
|
'ms.sort_order AS strategySortOrder',
|
||||||
|
'ms.title AS strategyTitle',
|
||||||
|
'ms.strategy_name AS strategyName',
|
||||||
|
'ms.parameters AS strategyParameters',
|
||||||
|
'ms.constraints AS strategyConstraints',
|
||||||
|
'ms.variants AS strategyVariants',
|
||||||
|
'mss.segment_id AS segmentId',
|
||||||
|
];
|
||||||
|
const processReleasePlanRows = (templateRows): ReleasePlan[] =>
|
||||||
|
templateRows.reduce(
|
||||||
|
(
|
||||||
|
acc: ReleasePlan[],
|
||||||
|
{
|
||||||
|
planId,
|
||||||
|
planDiscriminator,
|
||||||
|
planName,
|
||||||
|
planDescription,
|
||||||
|
planFeatureName,
|
||||||
|
planEnvironment,
|
||||||
|
planCreatedByUserId,
|
||||||
|
planCreatedAt,
|
||||||
|
planActiveMilestoneId,
|
||||||
|
planTemplateId,
|
||||||
|
milestoneId,
|
||||||
|
milestoneName,
|
||||||
|
milestoneSortOrder,
|
||||||
|
strategyId,
|
||||||
|
strategySortOrder,
|
||||||
|
strategyTitle,
|
||||||
|
strategyName,
|
||||||
|
strategyParameters,
|
||||||
|
strategyConstraints,
|
||||||
|
strategyVariants,
|
||||||
|
segmentId,
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
let plan = acc.find(({ id }) => id === planId);
|
||||||
|
|
||||||
|
if (!plan) {
|
||||||
|
plan = {
|
||||||
|
id: planId,
|
||||||
|
discriminator: planDiscriminator,
|
||||||
|
name: planName,
|
||||||
|
description: planDescription,
|
||||||
|
featureName: planFeatureName,
|
||||||
|
environment: planEnvironment,
|
||||||
|
createdByUserId: planCreatedByUserId,
|
||||||
|
createdAt: planCreatedAt,
|
||||||
|
activeMilestoneId: planActiveMilestoneId,
|
||||||
|
releasePlanTemplateId: planTemplateId,
|
||||||
|
milestones: [],
|
||||||
|
};
|
||||||
|
acc.push(plan);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!milestoneId) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
let milestone = plan.milestones.find(
|
||||||
|
({ id }) => id === milestoneId,
|
||||||
|
);
|
||||||
|
if (!milestone) {
|
||||||
|
milestone = {
|
||||||
|
id: milestoneId,
|
||||||
|
name: milestoneName,
|
||||||
|
sortOrder: milestoneSortOrder,
|
||||||
|
strategies: [],
|
||||||
|
releasePlanDefinitionId: planId,
|
||||||
|
};
|
||||||
|
plan.milestones.push(milestone);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!strategyId) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
let strategy = milestone.strategies?.find(
|
||||||
|
({ id }) => id === strategyId,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!strategy) {
|
||||||
|
strategy = {
|
||||||
|
id: strategyId,
|
||||||
|
milestoneId: milestoneId,
|
||||||
|
sortOrder: strategySortOrder,
|
||||||
|
title: strategyTitle,
|
||||||
|
strategyName: strategyName,
|
||||||
|
parameters: strategyParameters ?? {},
|
||||||
|
constraints: strategyConstraints,
|
||||||
|
variants: strategyVariants ?? [],
|
||||||
|
segments: [],
|
||||||
|
};
|
||||||
|
milestone.strategies = [
|
||||||
|
...(milestone.strategies || []),
|
||||||
|
strategy,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (segmentId) {
|
||||||
|
strategy.segments = [...(strategy.segments || []), segmentId];
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const processMilestoneStrategyRows = (
|
||||||
|
rows: any,
|
||||||
|
): ReleasePlanMilestoneStrategy[] => {
|
||||||
|
return rows.map((row) => {
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
sortOrder: row.sort_order,
|
||||||
|
title: row.title,
|
||||||
|
strategyName: row.strategy_name,
|
||||||
|
parameters: row.parameters,
|
||||||
|
constraints: row.constraints,
|
||||||
|
variants: row.variants,
|
||||||
|
segments: [],
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ReleasePlanStore extends CRUDStore<
|
||||||
|
ReleasePlan,
|
||||||
|
ReleasePlanWriteModel,
|
||||||
|
Row<ReleasePlan>,
|
||||||
|
ReleasePlan,
|
||||||
|
string
|
||||||
|
> {
|
||||||
|
constructor(db: Db, config: CrudStoreConfig) {
|
||||||
|
super(TABLE, db, config, {
|
||||||
|
fromRow: (row) => {
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
discriminator: row.discriminator,
|
||||||
|
name: row.name,
|
||||||
|
description: row.description,
|
||||||
|
featureName: row.feature_name,
|
||||||
|
environment: row.environment,
|
||||||
|
createdByUserId: row.created_by_user_id,
|
||||||
|
createdAt: row.created_at,
|
||||||
|
activeMilestoneId: row.active_milestone_id,
|
||||||
|
releasePlanTemplateId: row.release_plan_template_id,
|
||||||
|
milestones: [],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
toRow: (item) => ({
|
||||||
|
...defaultToRow(item),
|
||||||
|
discriminator: 'plan',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
override async count(
|
||||||
|
query?: Partial<ReleasePlanWriteModel>,
|
||||||
|
): Promise<number> {
|
||||||
|
let countQuery = this.db(this.tableName)
|
||||||
|
.where('discriminator', 'plan')
|
||||||
|
.count('*');
|
||||||
|
if (query) {
|
||||||
|
countQuery = countQuery.where(this.toRow(query));
|
||||||
|
}
|
||||||
|
const { count } = (await countQuery.first()) ?? { count: 0 };
|
||||||
|
return Number(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getByFeatureFlagEnvironmentAndPlanId(
|
||||||
|
featureName: string,
|
||||||
|
environment: string,
|
||||||
|
planId: string,
|
||||||
|
): Promise<ReleasePlan> {
|
||||||
|
const endTimer = this.timer('getByFeatureFlagEnvironmentAndPlanId');
|
||||||
|
const rows = await this.db(`${this.tableName} AS rpd`)
|
||||||
|
.where('rpd.discriminator', 'plan')
|
||||||
|
.andWhere('rpd.feature_name', featureName)
|
||||||
|
.andWhere('rpd.environment', environment)
|
||||||
|
.andWhere('rpd.id', planId)
|
||||||
|
.leftJoin(
|
||||||
|
'milestones AS mi',
|
||||||
|
'mi.release_plan_definition_id',
|
||||||
|
'rpd.id',
|
||||||
|
)
|
||||||
|
.leftJoin('milestone_strategies AS ms', 'ms.milestone_id', 'mi.id')
|
||||||
|
.leftJoin(
|
||||||
|
'milestone_strategy_segments AS mss',
|
||||||
|
'mss.milestone_strategy_id',
|
||||||
|
'ms.id',
|
||||||
|
)
|
||||||
|
.orderBy('mi.sort_order', 'asc')
|
||||||
|
.orderBy('ms.sort_order', 'asc')
|
||||||
|
.select(selectColumns);
|
||||||
|
endTimer();
|
||||||
|
return processReleasePlanRows(rows)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
async getByPlanId(planId: string): Promise<ReleasePlan | undefined> {
|
||||||
|
const endTimer = this.timer('getByPlanId');
|
||||||
|
const rows = await this.db(`${this.tableName} AS rpd`)
|
||||||
|
.where('rpd.discriminator', 'plan')
|
||||||
|
.andWhere('rpd.id', planId)
|
||||||
|
.leftJoin(
|
||||||
|
'milestones AS mi',
|
||||||
|
'mi.release_plan_definition_id',
|
||||||
|
'rpd.id',
|
||||||
|
)
|
||||||
|
.leftJoin('milestone_strategies AS ms', 'ms.milestone_id', 'mi.id')
|
||||||
|
.leftJoin(
|
||||||
|
'milestone_strategy_segments AS mss',
|
||||||
|
'mss.milestone_strategy_id',
|
||||||
|
'ms.id',
|
||||||
|
)
|
||||||
|
.orderBy('mi.sort_order', 'asc')
|
||||||
|
.orderBy('ms.sort_order', 'asc')
|
||||||
|
.select(selectColumns);
|
||||||
|
endTimer();
|
||||||
|
const releasePlans = processReleasePlanRows(rows);
|
||||||
|
if (releasePlans.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return releasePlans[0];
|
||||||
|
}
|
||||||
|
async getByFeatureFlagAndEnvironment(
|
||||||
|
featureName: string,
|
||||||
|
environment: string,
|
||||||
|
): Promise<ReleasePlan[]> {
|
||||||
|
const endTimer = this.timer('getByFeatureFlagAndEnvironment');
|
||||||
|
const planRows = await this.db(`${this.tableName} AS rpd`)
|
||||||
|
.where('rpd.discriminator', 'plan')
|
||||||
|
.andWhere('rpd.feature_name', featureName)
|
||||||
|
.andWhere('rpd.environment', environment)
|
||||||
|
.leftJoin(
|
||||||
|
'milestones AS mi',
|
||||||
|
'mi.release_plan_definition_id',
|
||||||
|
'rpd.id',
|
||||||
|
)
|
||||||
|
.leftJoin('milestone_strategies AS ms', 'ms.milestone_id', 'mi.id')
|
||||||
|
.leftJoin(
|
||||||
|
'milestone_strategy_segments AS mss',
|
||||||
|
'mss.milestone_strategy_id',
|
||||||
|
'ms.id',
|
||||||
|
)
|
||||||
|
.orderBy('mi.sort_order', 'asc')
|
||||||
|
.orderBy('ms.sort_order', 'asc')
|
||||||
|
.select(selectColumns);
|
||||||
|
endTimer();
|
||||||
|
return processReleasePlanRows(planRows);
|
||||||
|
}
|
||||||
|
|
||||||
|
async activateStrategiesForMilestone(
|
||||||
|
planId: string,
|
||||||
|
auditUser: IAuditUser,
|
||||||
|
): Promise<ReleasePlanMilestoneStrategy[]> {
|
||||||
|
const endTimer = this.timer('activateStrategiesForMilestone');
|
||||||
|
const rows = await this.db.raw(
|
||||||
|
`
|
||||||
|
INSERT INTO feature_strategies(id, feature_name, project_name, environment, strategy_name, parameters, constraints, sort_order, title, variants, created_by_user_id, milestone_id)
|
||||||
|
SELECT ms.id, rpd.feature_name, feature.project, rpd.environment, ms.strategy_name, ms.parameters, ms.constraints, ms.sort_order, ms.title, ms.variants, :userId, ms.milestone_id
|
||||||
|
FROM milestone_strategies AS ms
|
||||||
|
LEFT JOIN milestones AS m ON m.id = ms.milestone_id
|
||||||
|
LEFT JOIN release_plan_definitions AS rpd ON rpd.active_milestone_id = m.id AND rpd.discriminator = 'plan'
|
||||||
|
LEFT JOIN features AS feature ON rpd.feature_name = feature.name
|
||||||
|
WHERE rpd.id = :planId AND rpd.discriminator = 'plan'
|
||||||
|
ON CONFLICT DO NOTHING
|
||||||
|
RETURNING *
|
||||||
|
`,
|
||||||
|
{ userId: auditUser.id, planId },
|
||||||
|
);
|
||||||
|
endTimer();
|
||||||
|
return processMilestoneStrategyRows(rows.rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
async deactivateStrategiesForMilestone(
|
||||||
|
templateId: string,
|
||||||
|
): Promise<ReleasePlanMilestoneStrategy[]> {
|
||||||
|
const endTimer = this.timer('deactivateStrategiesForMilestone');
|
||||||
|
const deletedRows = await this.db.raw(
|
||||||
|
`DELETE FROM feature_strategies
|
||||||
|
WHERE milestone_id = (SELECT active_milestone_id FROM release_plan_definitions WHERE id = :templateId)
|
||||||
|
RETURNING *`,
|
||||||
|
{ templateId },
|
||||||
|
);
|
||||||
|
endTimer();
|
||||||
|
return processMilestoneStrategyRows(deletedRows.rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getActiveStrategiesForPlan(
|
||||||
|
planId: string,
|
||||||
|
): Promise<ReleasePlanMilestoneStrategy[]> {
|
||||||
|
const endTimer = this.timer('getActiveStrategiesForPlan');
|
||||||
|
const rows = await this.db
|
||||||
|
.select(
|
||||||
|
'id',
|
||||||
|
'strategy_name',
|
||||||
|
'sort_order',
|
||||||
|
'title',
|
||||||
|
'parameters',
|
||||||
|
'variants',
|
||||||
|
'constraints',
|
||||||
|
)
|
||||||
|
.from('feature_strategies')
|
||||||
|
.whereRaw(
|
||||||
|
`milestone_id IN (SELECT active_milestone_id FROM release_plan_definitions WHERE id = :id)`,
|
||||||
|
{ id: planId },
|
||||||
|
);
|
||||||
|
endTimer();
|
||||||
|
return processMilestoneStrategyRows(rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
async activateStrategySegmentsForMilestone(
|
||||||
|
milestone_id: string,
|
||||||
|
): Promise<number[]> {
|
||||||
|
const endTimer = this.timer('activateStrategySegmentsForMilestone');
|
||||||
|
const rows = await this.db.raw(
|
||||||
|
`
|
||||||
|
INSERT INTO feature_strategy_segment(feature_strategy_id, segment_id) SELECT milestone_strategy_id, segment_id FROM milestone_strategy_segments WHERE milestone_strategy_id IN (SELECT id FROM milestone_strategies WHERE milestone_id = :milestone_id) RETURNING segment_id
|
||||||
|
`,
|
||||||
|
{ milestone_id },
|
||||||
|
);
|
||||||
|
endTimer();
|
||||||
|
return rows.rows.map((row) => row.segment_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
async featureAndEnvironmentHasPlan(
|
||||||
|
featureName: string,
|
||||||
|
environment: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const endTimer = this.timer('featureAndEnvironmentHasPlan');
|
||||||
|
const result = await this.db.raw(
|
||||||
|
`SELECT EXISTS (SELECT 1 FROM ${TABLE} WHERE discriminator = 'plan' AND feature_name = :featureName AND environment = :environment) AS present`,
|
||||||
|
{ featureName, environment },
|
||||||
|
);
|
||||||
|
const { present } = result.rows[0];
|
||||||
|
endTimer();
|
||||||
|
return present;
|
||||||
|
}
|
||||||
|
}
|
205
src/lib/features/release-plans/release-plan-template-store.ts
Normal file
205
src/lib/features/release-plans/release-plan-template-store.ts
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
import { ulid } from 'ulidx';
|
||||||
|
import type { ReleasePlanTemplate } from './release-plan-template';
|
||||||
|
import type { ReleasePlanMilestone } from './release-plan-milestone';
|
||||||
|
import { CRUDStore, type CrudStoreConfig } from '../../db/crud/crud-store';
|
||||||
|
import type { Row } from '../../db/crud/row-type';
|
||||||
|
import type { Db } from '../../db/db';
|
||||||
|
import { NotFoundError } from '../../error';
|
||||||
|
|
||||||
|
const TABLE = 'release_plan_definitions';
|
||||||
|
|
||||||
|
export type ReleasePlanTemplateWriteModel = Omit<
|
||||||
|
ReleasePlanTemplate,
|
||||||
|
'id' | 'createdAt' | 'milestones'
|
||||||
|
>;
|
||||||
|
|
||||||
|
const fromRow = (row: any): ReleasePlanTemplate => {
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
name: row.name,
|
||||||
|
createdAt: row.created_at,
|
||||||
|
description: row.description,
|
||||||
|
discriminator: row.discriminator,
|
||||||
|
createdByUserId: row.created_by_user_id,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ReleasePlanTemplateStore extends CRUDStore<
|
||||||
|
ReleasePlanTemplate,
|
||||||
|
ReleasePlanTemplateWriteModel,
|
||||||
|
Row<ReleasePlanTemplate>,
|
||||||
|
ReleasePlanTemplate,
|
||||||
|
string
|
||||||
|
> {
|
||||||
|
constructor(db: Db, config: CrudStoreConfig) {
|
||||||
|
super(TABLE, db, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
override async getAll(): Promise<ReleasePlanTemplate[]> {
|
||||||
|
const endTimer = this.timer('getAll');
|
||||||
|
const templates = await this.db<ReleasePlanTemplate>(TABLE)
|
||||||
|
.where('discriminator', 'template')
|
||||||
|
.where('archived_at', null)
|
||||||
|
.orderBy('created_at');
|
||||||
|
endTimer();
|
||||||
|
return templates.map(({ milestones, ...template }) =>
|
||||||
|
fromRow(template),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
override async count(
|
||||||
|
query?: Partial<ReleasePlanTemplateWriteModel>,
|
||||||
|
): Promise<number> {
|
||||||
|
let countQuery = this.db(this.tableName)
|
||||||
|
.where('discriminator', 'template')
|
||||||
|
.whereNull('archived_at')
|
||||||
|
.count('*');
|
||||||
|
if (query) {
|
||||||
|
countQuery = countQuery.where(this.toRow(query));
|
||||||
|
}
|
||||||
|
const { count } = (await countQuery.first()) ?? { count: 0 };
|
||||||
|
return Number(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
async checkNameAlreadyExists(name: string, id?: string): Promise<boolean> {
|
||||||
|
const exists = await this.db(TABLE)
|
||||||
|
.where('discriminator', 'template')
|
||||||
|
.where({ name })
|
||||||
|
.modify((qb) => {
|
||||||
|
if (id) {
|
||||||
|
qb.whereNot('id', id);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.first()
|
||||||
|
.select('id');
|
||||||
|
|
||||||
|
return Boolean(exists);
|
||||||
|
}
|
||||||
|
|
||||||
|
processReleasePlanTemplateRows(templateRows): ReleasePlanTemplate {
|
||||||
|
return {
|
||||||
|
id: templateRows[0].templateId,
|
||||||
|
discriminator: templateRows[0].templateDiscriminator,
|
||||||
|
name: templateRows[0].templateName,
|
||||||
|
description: templateRows[0].templateDescription,
|
||||||
|
createdByUserId: templateRows[0].templateCreatedByUserId,
|
||||||
|
createdAt: templateRows[0].templateCreatedAt,
|
||||||
|
milestones: templateRows.reduce(
|
||||||
|
(acc: ReleasePlanMilestone[], row) => {
|
||||||
|
if (!row.milestoneId) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
let milestone = acc.find((m) => m.id === row.milestoneId);
|
||||||
|
if (!milestone) {
|
||||||
|
milestone = {
|
||||||
|
id: row.milestoneId,
|
||||||
|
name: row.milestoneName,
|
||||||
|
sortOrder: row.milestoneSortOrder,
|
||||||
|
strategies: [],
|
||||||
|
releasePlanDefinitionId: row.templateId,
|
||||||
|
};
|
||||||
|
acc.push(milestone);
|
||||||
|
}
|
||||||
|
if (!row.strategyId) {
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
let strategy = milestone.strategies?.find(
|
||||||
|
(s) => s.id === row.strategyId,
|
||||||
|
);
|
||||||
|
if (!strategy) {
|
||||||
|
strategy = {
|
||||||
|
id: row.strategyId,
|
||||||
|
milestoneId: row.milestoneId,
|
||||||
|
sortOrder: row.strategySortOrder,
|
||||||
|
title: row.strategyTitle,
|
||||||
|
strategyName: row.strategyName,
|
||||||
|
parameters: row.strategyParameters ?? {},
|
||||||
|
constraints: row.strategyConstraints,
|
||||||
|
variants: row.strategyVariants ?? [],
|
||||||
|
segments: [],
|
||||||
|
};
|
||||||
|
milestone.strategies = [
|
||||||
|
...(milestone.strategies || []),
|
||||||
|
strategy,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (row.segmentId) {
|
||||||
|
strategy.segments = [
|
||||||
|
...(strategy.segments || []),
|
||||||
|
row.segmentId,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
),
|
||||||
|
archivedAt: templateRows[0].templateArchivedAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async getById(id: string): Promise<ReleasePlanTemplate> {
|
||||||
|
const endTimer = this.timer('getById');
|
||||||
|
const templateRows = await this.db(`${TABLE} AS rpd`)
|
||||||
|
.where('rpd.id', id)
|
||||||
|
.leftJoin(
|
||||||
|
'milestones AS mi',
|
||||||
|
'mi.release_plan_definition_id',
|
||||||
|
'rpd.id',
|
||||||
|
)
|
||||||
|
.leftJoin('milestone_strategies AS ms', 'ms.milestone_id', 'mi.id')
|
||||||
|
.leftJoin(
|
||||||
|
'milestone_strategy_segments AS mss',
|
||||||
|
'mss.milestone_strategy_id',
|
||||||
|
'ms.id',
|
||||||
|
)
|
||||||
|
.orderBy('mi.sort_order', 'asc')
|
||||||
|
.orderBy('ms.sort_order', 'asc')
|
||||||
|
.select(
|
||||||
|
'rpd.id AS templateId',
|
||||||
|
'rpd.discriminator AS templateDiscriminator',
|
||||||
|
'rpd.name AS templateName',
|
||||||
|
'rpd.description as templateDescription',
|
||||||
|
'rpd.created_by_user_id as templateCreatedByUserId',
|
||||||
|
'rpd.created_at as templateCreatedAt',
|
||||||
|
'rpd.archived_at AS templateArchivedAt',
|
||||||
|
'mi.id AS milestoneId',
|
||||||
|
'mi.name AS milestoneName',
|
||||||
|
'mi.sort_order AS milestoneSortOrder',
|
||||||
|
'ms.id AS strategyId',
|
||||||
|
'ms.sort_order AS strategySortOrder',
|
||||||
|
'ms.title AS strategyTitle',
|
||||||
|
'ms.strategy_name AS strategyName',
|
||||||
|
'ms.parameters AS strategyParameters',
|
||||||
|
'ms.constraints AS strategyConstraints',
|
||||||
|
'ms.variants AS strategyVariants',
|
||||||
|
'mss.segment_id AS segmentId',
|
||||||
|
);
|
||||||
|
endTimer();
|
||||||
|
|
||||||
|
if (!templateRows.length) {
|
||||||
|
throw new NotFoundError(`Could not find template with id ${id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.processReleasePlanTemplateRows(templateRows);
|
||||||
|
}
|
||||||
|
|
||||||
|
override async insert(
|
||||||
|
item: ReleasePlanTemplateWriteModel,
|
||||||
|
): Promise<ReleasePlanTemplate> {
|
||||||
|
const endTimer = this.timer('insert');
|
||||||
|
const row = this.toRow(item);
|
||||||
|
row.id = ulid();
|
||||||
|
await this.db(TABLE).insert(row);
|
||||||
|
endTimer();
|
||||||
|
return fromRow(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
async archive(id: string): Promise<void> {
|
||||||
|
const endTimer = this.timer('archive');
|
||||||
|
const now = new Date();
|
||||||
|
await this.db(TABLE).where('id', id).update({ archived_at: now });
|
||||||
|
endTimer();
|
||||||
|
}
|
||||||
|
}
|
12
src/lib/features/release-plans/release-plan-template.ts
Normal file
12
src/lib/features/release-plans/release-plan-template.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import type { ReleasePlanMilestone } from './release-plan-milestone';
|
||||||
|
|
||||||
|
export interface ReleasePlanTemplate {
|
||||||
|
id: string;
|
||||||
|
discriminator: 'template';
|
||||||
|
name: string;
|
||||||
|
description?: string | null;
|
||||||
|
createdByUserId: number;
|
||||||
|
createdAt: string;
|
||||||
|
milestones?: ReleasePlanMilestone[];
|
||||||
|
archivedAt?: string;
|
||||||
|
}
|
15
src/lib/features/release-plans/release-plan.ts
Normal file
15
src/lib/features/release-plans/release-plan.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import type { ReleasePlanMilestone } from './release-plan-milestone';
|
||||||
|
|
||||||
|
export interface ReleasePlan {
|
||||||
|
id: string;
|
||||||
|
discriminator: 'plan';
|
||||||
|
name: string;
|
||||||
|
description?: string | null;
|
||||||
|
featureName: string;
|
||||||
|
environment: string;
|
||||||
|
createdByUserId: number;
|
||||||
|
createdAt: string;
|
||||||
|
activeMilestoneId?: string;
|
||||||
|
milestones: ReleasePlanMilestone[];
|
||||||
|
releasePlanTemplateId: string;
|
||||||
|
}
|
@ -55,6 +55,10 @@ import type { IUserUnsubscribeStore } from '../features/user-subscriptions/user-
|
|||||||
import type { IUserSubscriptionsReadModel } from '../features/user-subscriptions/user-subscriptions-read-model-type';
|
import type { IUserSubscriptionsReadModel } from '../features/user-subscriptions/user-subscriptions-read-model-type';
|
||||||
import { IUniqueConnectionStore } from '../features/unique-connection/unique-connection-store-type';
|
import { IUniqueConnectionStore } from '../features/unique-connection/unique-connection-store-type';
|
||||||
import { IUniqueConnectionReadModel } from '../features/unique-connection/unique-connection-read-model-type';
|
import { IUniqueConnectionReadModel } from '../features/unique-connection/unique-connection-read-model-type';
|
||||||
|
import { ReleasePlanStore } from '../features/release-plans/release-plan-store';
|
||||||
|
import { ReleasePlanTemplateStore } from '../features/release-plans/release-plan-template-store';
|
||||||
|
import { ReleasePlanMilestoneStore } from '../features/release-plans/release-plan-milestone-store';
|
||||||
|
import { ReleasePlanMilestoneStrategyStore } from '../features/release-plans/release-plan-milestone-strategy-store';
|
||||||
|
|
||||||
export interface IUnleashStores {
|
export interface IUnleashStores {
|
||||||
accessStore: IAccessStore;
|
accessStore: IAccessStore;
|
||||||
@ -114,6 +118,10 @@ export interface IUnleashStores {
|
|||||||
userSubscriptionsReadModel: IUserSubscriptionsReadModel;
|
userSubscriptionsReadModel: IUserSubscriptionsReadModel;
|
||||||
uniqueConnectionStore: IUniqueConnectionStore;
|
uniqueConnectionStore: IUniqueConnectionStore;
|
||||||
uniqueConnectionReadModel: IUniqueConnectionReadModel;
|
uniqueConnectionReadModel: IUniqueConnectionReadModel;
|
||||||
|
releasePlanStore: ReleasePlanStore;
|
||||||
|
releasePlanTemplateStore: ReleasePlanTemplateStore;
|
||||||
|
releasePlanMilestoneStore: ReleasePlanMilestoneStore;
|
||||||
|
releasePlanMilestoneStrategyStore: ReleasePlanMilestoneStrategyStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@ -171,4 +179,8 @@ export {
|
|||||||
type IUserSubscriptionsReadModel,
|
type IUserSubscriptionsReadModel,
|
||||||
IUniqueConnectionStore,
|
IUniqueConnectionStore,
|
||||||
IUniqueConnectionReadModel,
|
IUniqueConnectionReadModel,
|
||||||
|
ReleasePlanStore,
|
||||||
|
ReleasePlanTemplateStore,
|
||||||
|
ReleasePlanMilestoneStore,
|
||||||
|
ReleasePlanMilestoneStrategyStore,
|
||||||
};
|
};
|
||||||
|
9
src/test/fixtures/store.ts
vendored
9
src/test/fixtures/store.ts
vendored
@ -20,6 +20,10 @@ import type {
|
|||||||
IntegrationEventsStore,
|
IntegrationEventsStore,
|
||||||
IPrivateProjectStore,
|
IPrivateProjectStore,
|
||||||
IUnleashStores,
|
IUnleashStores,
|
||||||
|
ReleasePlanMilestoneStore,
|
||||||
|
ReleasePlanMilestoneStrategyStore,
|
||||||
|
ReleasePlanStore,
|
||||||
|
ReleasePlanTemplateStore,
|
||||||
} from '../../lib/types';
|
} from '../../lib/types';
|
||||||
import FakeSessionStore from './fake-session-store';
|
import FakeSessionStore from './fake-session-store';
|
||||||
import FakeFeatureEnvironmentStore from './fake-feature-environment-store';
|
import FakeFeatureEnvironmentStore from './fake-feature-environment-store';
|
||||||
@ -129,6 +133,11 @@ const createStores: () => IUnleashStores = () => {
|
|||||||
uniqueConnectionReadModel: new UniqueConnectionReadModel(
|
uniqueConnectionReadModel: new UniqueConnectionReadModel(
|
||||||
uniqueConnectionStore,
|
uniqueConnectionStore,
|
||||||
),
|
),
|
||||||
|
releasePlanStore: {} as ReleasePlanStore,
|
||||||
|
releasePlanMilestoneStore: {} as ReleasePlanMilestoneStore,
|
||||||
|
releasePlanTemplateStore: {} as ReleasePlanTemplateStore,
|
||||||
|
releasePlanMilestoneStrategyStore:
|
||||||
|
{} as ReleasePlanMilestoneStrategyStore,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
17
yarn.lock
17
yarn.lock
@ -6246,6 +6246,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"layerr@npm:^3.0.0":
|
||||||
|
version: 3.0.0
|
||||||
|
resolution: "layerr@npm:3.0.0"
|
||||||
|
checksum: 10c0/320c9b9cf1392c73c9ff8f8d1bb7a782093ac7341fe5d7fea6ebfeea6d785af8e0fc573541431b6ffcb268577f8aea66168757e73a15035568a00ab9e4370705
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"lazy-cache@npm:^0.2.3":
|
"lazy-cache@npm:^0.2.3":
|
||||||
version: 0.2.7
|
version: 0.2.7
|
||||||
resolution: "lazy-cache@npm:0.2.7"
|
resolution: "lazy-cache@npm:0.2.7"
|
||||||
@ -9290,6 +9297,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"ulidx@npm:^2.4.1":
|
||||||
|
version: 2.4.1
|
||||||
|
resolution: "ulidx@npm:2.4.1"
|
||||||
|
dependencies:
|
||||||
|
layerr: "npm:^3.0.0"
|
||||||
|
checksum: 10c0/3cbe05123b4b49d262e26cd0edd3ac38a4f9e97f043d47ae7daa88502748881d1615d832e3bcb29698ca2429947c24494929ba35b40d6bf38df891cb15642d2e
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"undici-types@npm:~5.26.4":
|
"undici-types@npm:~5.26.4":
|
||||||
version: 5.26.5
|
version: 5.26.5
|
||||||
resolution: "undici-types@npm:5.26.5"
|
resolution: "undici-types@npm:5.26.5"
|
||||||
@ -9464,6 +9480,7 @@ __metadata:
|
|||||||
tsc-watch: "npm:6.2.1"
|
tsc-watch: "npm:6.2.1"
|
||||||
type-is: "npm:^1.6.18"
|
type-is: "npm:^1.6.18"
|
||||||
typescript: "npm:5.8.2"
|
typescript: "npm:5.8.2"
|
||||||
|
ulidx: "npm:^2.4.1"
|
||||||
unleash-client: "npm:^6.6.0"
|
unleash-client: "npm:^6.6.0"
|
||||||
uuid: "npm:^9.0.0"
|
uuid: "npm:^9.0.0"
|
||||||
wait-on: "npm:^8.0.0"
|
wait-on: "npm:^8.0.0"
|
||||||
|
Loading…
Reference in New Issue
Block a user