mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-24 01:18:01 +02:00
refactor: move search implementation out of strategies store (#5642)
This is first step of refactoring. Next steps follow with possibly a query builder, or atleast using some reusable methods.
This commit is contained in:
parent
fbb5dd9022
commit
fa087fb473
@ -39,6 +39,7 @@ import { ImportTogglesStore } from '../features/export-import-toggles/import-tog
|
|||||||
import PrivateProjectStore from '../features/private-project/privateProjectStore';
|
import PrivateProjectStore from '../features/private-project/privateProjectStore';
|
||||||
import { DependentFeaturesStore } from '../features/dependent-features/dependent-features-store';
|
import { DependentFeaturesStore } from '../features/dependent-features/dependent-features-store';
|
||||||
import LastSeenStore from '../services/client-metrics/last-seen/last-seen-store';
|
import LastSeenStore from '../services/client-metrics/last-seen/last-seen-store';
|
||||||
|
import FeatureSearchStore from '../features/feature-search/feature-search-store';
|
||||||
|
|
||||||
export const createStores = (
|
export const createStores = (
|
||||||
config: IUnleashConfig,
|
config: IUnleashConfig,
|
||||||
@ -139,6 +140,7 @@ export const createStores = (
|
|||||||
privateProjectStore: new PrivateProjectStore(db, getLogger),
|
privateProjectStore: new PrivateProjectStore(db, getLogger),
|
||||||
dependentFeaturesStore: new DependentFeaturesStore(db),
|
dependentFeaturesStore: new DependentFeaturesStore(db),
|
||||||
lastSeenStore: new LastSeenStore(db, eventBus, getLogger),
|
lastSeenStore: new LastSeenStore(db, eventBus, getLogger),
|
||||||
|
featureSearchStore: new FeatureSearchStore(db, eventBus, getLogger),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,23 +1,22 @@
|
|||||||
import { Db } from '../../db/db';
|
import { Db } from '../../db/db';
|
||||||
import { IUnleashConfig } from '../../types';
|
import { IUnleashConfig } from '../../types';
|
||||||
|
|
||||||
import FeatureStrategiesStore from '../feature-toggle/feature-toggle-strategies-store';
|
|
||||||
import { FeatureSearchService } from './feature-search-service';
|
import { FeatureSearchService } from './feature-search-service';
|
||||||
import FakeFeatureStrategiesStore from '../feature-toggle/fakes/fake-feature-strategies-store';
|
import FakeFeatureSearchStore from './fake-feature-search-store';
|
||||||
|
import FeatureSearchStore from './feature-search-store';
|
||||||
|
|
||||||
export const createFeatureSearchService =
|
export const createFeatureSearchService =
|
||||||
(config: IUnleashConfig) =>
|
(config: IUnleashConfig) =>
|
||||||
(db: Db): FeatureSearchService => {
|
(db: Db): FeatureSearchService => {
|
||||||
const { getLogger, eventBus, flagResolver } = config;
|
const { getLogger, eventBus, flagResolver } = config;
|
||||||
const featureStrategiesStore = new FeatureStrategiesStore(
|
const featureSearchStore = new FeatureSearchStore(
|
||||||
db,
|
db,
|
||||||
eventBus,
|
eventBus,
|
||||||
getLogger,
|
getLogger,
|
||||||
flagResolver,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return new FeatureSearchService(
|
return new FeatureSearchService(
|
||||||
{ featureStrategiesStore: featureStrategiesStore },
|
{ featureSearchStore: featureSearchStore },
|
||||||
config,
|
config,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@ -25,11 +24,11 @@ export const createFeatureSearchService =
|
|||||||
export const createFakeFeatureSearchService = (
|
export const createFakeFeatureSearchService = (
|
||||||
config: IUnleashConfig,
|
config: IUnleashConfig,
|
||||||
): FeatureSearchService => {
|
): FeatureSearchService => {
|
||||||
const fakeFeatureStrategiesStore = new FakeFeatureStrategiesStore();
|
const fakeFeatureSearchStore = new FakeFeatureSearchStore();
|
||||||
|
|
||||||
return new FeatureSearchService(
|
return new FeatureSearchService(
|
||||||
{
|
{
|
||||||
featureStrategiesStore: fakeFeatureStrategiesStore,
|
featureSearchStore: fakeFeatureSearchStore,
|
||||||
},
|
},
|
||||||
config,
|
config,
|
||||||
);
|
);
|
||||||
|
15
src/lib/features/feature-search/fake-feature-search-store.ts
Normal file
15
src/lib/features/feature-search/fake-feature-search-store.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import { IFeatureOverview } from 'lib/types';
|
||||||
|
import {
|
||||||
|
IFeatureSearchParams,
|
||||||
|
IQueryParam,
|
||||||
|
} from '../feature-toggle/types/feature-toggle-strategies-store-type';
|
||||||
|
import { IFeatureSearchStore } from './feature-search-store-type';
|
||||||
|
|
||||||
|
export default class FakeFeatureSearchStore implements IFeatureSearchStore {
|
||||||
|
searchFeatures(
|
||||||
|
params: IFeatureSearchParams,
|
||||||
|
queryParams: IQueryParam[],
|
||||||
|
): Promise<{ features: IFeatureOverview[]; total: number }> {
|
||||||
|
throw new Error('Method not implemented.');
|
||||||
|
}
|
||||||
|
}
|
@ -1,9 +1,8 @@
|
|||||||
import { Logger } from '../../logger';
|
import { Logger } from '../../logger';
|
||||||
import {
|
import {
|
||||||
IFeatureStrategiesStore,
|
IFeatureSearchStore,
|
||||||
IUnleashConfig,
|
IUnleashConfig,
|
||||||
IUnleashStores,
|
IUnleashStores,
|
||||||
serializeDates,
|
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
import {
|
import {
|
||||||
IFeatureSearchParams,
|
IFeatureSearchParams,
|
||||||
@ -12,22 +11,20 @@ import {
|
|||||||
} from '../feature-toggle/types/feature-toggle-strategies-store-type';
|
} from '../feature-toggle/types/feature-toggle-strategies-store-type';
|
||||||
|
|
||||||
export class FeatureSearchService {
|
export class FeatureSearchService {
|
||||||
private featureStrategiesStore: IFeatureStrategiesStore;
|
private featureSearchStore: IFeatureSearchStore;
|
||||||
private logger: Logger;
|
private logger: Logger;
|
||||||
constructor(
|
constructor(
|
||||||
{
|
{ featureSearchStore }: Pick<IUnleashStores, 'featureSearchStore'>,
|
||||||
featureStrategiesStore,
|
|
||||||
}: Pick<IUnleashStores, 'featureStrategiesStore'>,
|
|
||||||
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>,
|
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>,
|
||||||
) {
|
) {
|
||||||
this.featureStrategiesStore = featureStrategiesStore;
|
this.featureSearchStore = featureSearchStore;
|
||||||
this.logger = getLogger('services/feature-search-service.ts');
|
this.logger = getLogger('services/feature-search-service.ts');
|
||||||
}
|
}
|
||||||
|
|
||||||
async search(params: IFeatureSearchParams) {
|
async search(params: IFeatureSearchParams) {
|
||||||
const queryParams = this.convertToQueryParams(params);
|
const queryParams = this.convertToQueryParams(params);
|
||||||
const { features, total } =
|
const { features, total } =
|
||||||
await this.featureStrategiesStore.searchFeatures(
|
await this.featureSearchStore.searchFeatures(
|
||||||
{
|
{
|
||||||
...params,
|
...params,
|
||||||
limit: params.limit,
|
limit: params.limit,
|
||||||
|
15
src/lib/features/feature-search/feature-search-store-type.ts
Normal file
15
src/lib/features/feature-search/feature-search-store-type.ts
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
import {
|
||||||
|
IFeatureSearchParams,
|
||||||
|
IQueryParam,
|
||||||
|
} from '../feature-toggle/types/feature-toggle-strategies-store-type';
|
||||||
|
import { IFeatureOverview } from '../../types';
|
||||||
|
|
||||||
|
export interface IFeatureSearchStore {
|
||||||
|
searchFeatures(
|
||||||
|
params: IFeatureSearchParams,
|
||||||
|
queryParams: IQueryParam[],
|
||||||
|
): Promise<{
|
||||||
|
features: IFeatureOverview[];
|
||||||
|
total: number;
|
||||||
|
}>;
|
||||||
|
}
|
520
src/lib/features/feature-search/feature-search-store.ts
Normal file
520
src/lib/features/feature-search/feature-search-store.ts
Normal file
@ -0,0 +1,520 @@
|
|||||||
|
import { Knex } from 'knex';
|
||||||
|
import EventEmitter from 'events';
|
||||||
|
import metricsHelper from '../../util/metrics-helper';
|
||||||
|
import { DB_TIME } from '../../metric-events';
|
||||||
|
import { Logger, LogProvider } from '../../logger';
|
||||||
|
import {
|
||||||
|
IEnvironmentOverview,
|
||||||
|
IFeatureOverview,
|
||||||
|
IFeatureSearchStore,
|
||||||
|
IFlagResolver,
|
||||||
|
ITag,
|
||||||
|
} from '../../types';
|
||||||
|
import FeatureToggleStore from '../feature-toggle/feature-toggle-store';
|
||||||
|
import { Db } from '../../db/db';
|
||||||
|
import Raw = Knex.Raw;
|
||||||
|
import {
|
||||||
|
IFeatureSearchParams,
|
||||||
|
IQueryParam,
|
||||||
|
} from '../feature-toggle/types/feature-toggle-strategies-store-type';
|
||||||
|
import FeatureStrategiesStore from '../feature-toggle/feature-toggle-strategies-store';
|
||||||
|
|
||||||
|
const sortEnvironments = (overview: IFeatureOverview) => {
|
||||||
|
return Object.values(overview).map((data: IFeatureOverview) => ({
|
||||||
|
...data,
|
||||||
|
environments: data.environments
|
||||||
|
.filter((f) => f.name)
|
||||||
|
.sort((a, b) => {
|
||||||
|
if (a.sortOrder === b.sortOrder) {
|
||||||
|
return a.name.localeCompare(b.name);
|
||||||
|
}
|
||||||
|
return a.sortOrder - b.sortOrder;
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
class FeatureSearchStore implements IFeatureSearchStore {
|
||||||
|
private db: Db;
|
||||||
|
|
||||||
|
private logger: Logger;
|
||||||
|
|
||||||
|
private readonly timer: Function;
|
||||||
|
|
||||||
|
constructor(db: Db, eventBus: EventEmitter, getLogger: LogProvider) {
|
||||||
|
this.db = db;
|
||||||
|
this.logger = getLogger('feature-search-store.ts');
|
||||||
|
this.timer = (action) =>
|
||||||
|
metricsHelper.wrapTimer(eventBus, DB_TIME, {
|
||||||
|
store: 'feature-search',
|
||||||
|
action,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static getEnvironment(r: any): IEnvironmentOverview {
|
||||||
|
return {
|
||||||
|
name: r.environment,
|
||||||
|
enabled: r.enabled,
|
||||||
|
type: r.environment_type,
|
||||||
|
sortOrder: r.environment_sort_order,
|
||||||
|
variantCount: r.variants?.length || 0,
|
||||||
|
lastSeenAt: r.env_last_seen_at,
|
||||||
|
hasStrategies: r.has_strategies,
|
||||||
|
hasEnabledStrategies: r.has_enabled_strategies,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async searchFeatures(
|
||||||
|
{
|
||||||
|
userId,
|
||||||
|
searchParams,
|
||||||
|
type,
|
||||||
|
status,
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
sortOrder,
|
||||||
|
sortBy,
|
||||||
|
favoritesFirst,
|
||||||
|
}: IFeatureSearchParams,
|
||||||
|
queryParams: IQueryParam[],
|
||||||
|
): Promise<{
|
||||||
|
features: IFeatureOverview[];
|
||||||
|
total: number;
|
||||||
|
}> {
|
||||||
|
const stopTimer = this.timer('searchFeatures');
|
||||||
|
const validatedSortOrder =
|
||||||
|
sortOrder === 'asc' || sortOrder === 'desc' ? sortOrder : 'asc';
|
||||||
|
|
||||||
|
const finalQuery = this.db
|
||||||
|
.with('ranked_features', (query) => {
|
||||||
|
query.from('features');
|
||||||
|
|
||||||
|
applyQueryParams(query, queryParams);
|
||||||
|
|
||||||
|
const hasSearchParams = searchParams?.length;
|
||||||
|
if (hasSearchParams) {
|
||||||
|
const sqlParameters = searchParams.map(
|
||||||
|
(item) => `%${item}%`,
|
||||||
|
);
|
||||||
|
const sqlQueryParameters = sqlParameters
|
||||||
|
.map(() => '?')
|
||||||
|
.join(',');
|
||||||
|
|
||||||
|
query.where((builder) => {
|
||||||
|
builder
|
||||||
|
.orWhereRaw(
|
||||||
|
`(??) ILIKE ANY (ARRAY[${sqlQueryParameters}])`,
|
||||||
|
['features.name', ...sqlParameters],
|
||||||
|
)
|
||||||
|
.orWhereRaw(
|
||||||
|
`(??) ILIKE ANY (ARRAY[${sqlQueryParameters}])`,
|
||||||
|
['features.description', ...sqlParameters],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type) {
|
||||||
|
query.whereIn('features.type', type);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status && status.length > 0) {
|
||||||
|
query.where((builder) => {
|
||||||
|
for (const [envName, envStatus] of status) {
|
||||||
|
builder.orWhere(function () {
|
||||||
|
this.where(
|
||||||
|
'feature_environments.environment',
|
||||||
|
envName,
|
||||||
|
).andWhere(
|
||||||
|
'feature_environments.enabled',
|
||||||
|
envStatus === 'enabled' ? true : false,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
query
|
||||||
|
.modify(FeatureToggleStore.filterByArchived, false)
|
||||||
|
.leftJoin(
|
||||||
|
'feature_environments',
|
||||||
|
'feature_environments.feature_name',
|
||||||
|
'features.name',
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
'environments',
|
||||||
|
'feature_environments.environment',
|
||||||
|
'environments.name',
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
'feature_tag as ft',
|
||||||
|
'ft.feature_name',
|
||||||
|
'features.name',
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
'feature_strategies',
|
||||||
|
'feature_strategies.feature_name',
|
||||||
|
'features.name',
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
'feature_strategy_segment',
|
||||||
|
'feature_strategy_segment.feature_strategy_id',
|
||||||
|
'feature_strategies.id',
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
'segments',
|
||||||
|
'feature_strategy_segment.segment_id',
|
||||||
|
'segments.id',
|
||||||
|
);
|
||||||
|
|
||||||
|
query.leftJoin('last_seen_at_metrics', function () {
|
||||||
|
this.on(
|
||||||
|
'last_seen_at_metrics.environment',
|
||||||
|
'=',
|
||||||
|
'environments.name',
|
||||||
|
).andOn(
|
||||||
|
'last_seen_at_metrics.feature_name',
|
||||||
|
'=',
|
||||||
|
'features.name',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
let selectColumns = [
|
||||||
|
'features.name as feature_name',
|
||||||
|
'features.description as description',
|
||||||
|
'features.type as type',
|
||||||
|
'features.project as project',
|
||||||
|
'features.created_at as created_at',
|
||||||
|
'features.stale as stale',
|
||||||
|
'features.last_seen_at as last_seen_at',
|
||||||
|
'features.impression_data as impression_data',
|
||||||
|
'feature_environments.enabled as enabled',
|
||||||
|
'feature_environments.environment as environment',
|
||||||
|
'feature_environments.variants as variants',
|
||||||
|
'environments.type as environment_type',
|
||||||
|
'environments.sort_order as environment_sort_order',
|
||||||
|
'ft.tag_value as tag_value',
|
||||||
|
'ft.tag_type as tag_type',
|
||||||
|
'segments.name as segment_name',
|
||||||
|
] as (string | Raw<any> | Knex.QueryBuilder)[];
|
||||||
|
|
||||||
|
const lastSeenQuery = 'last_seen_at_metrics.last_seen_at';
|
||||||
|
selectColumns.push(`${lastSeenQuery} as env_last_seen_at`);
|
||||||
|
|
||||||
|
if (userId) {
|
||||||
|
query.leftJoin(`favorite_features`, function () {
|
||||||
|
this.on(
|
||||||
|
'favorite_features.feature',
|
||||||
|
'features.name',
|
||||||
|
).andOnVal('favorite_features.user_id', '=', userId);
|
||||||
|
});
|
||||||
|
selectColumns = [
|
||||||
|
...selectColumns,
|
||||||
|
this.db.raw(
|
||||||
|
'favorite_features.feature is not null as favorite',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
selectColumns = [
|
||||||
|
...selectColumns,
|
||||||
|
this.db.raw(
|
||||||
|
'EXISTS (SELECT 1 FROM feature_strategies WHERE feature_strategies.feature_name = features.name AND feature_strategies.environment = feature_environments.environment) as has_strategies',
|
||||||
|
),
|
||||||
|
this.db.raw(
|
||||||
|
'EXISTS (SELECT 1 FROM feature_strategies WHERE feature_strategies.feature_name = features.name AND feature_strategies.environment = feature_environments.environment AND (feature_strategies.disabled IS NULL OR feature_strategies.disabled = false)) as has_enabled_strategies',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
const sortByMapping = {
|
||||||
|
name: 'features.name',
|
||||||
|
type: 'features.type',
|
||||||
|
lastSeenAt: lastSeenQuery,
|
||||||
|
};
|
||||||
|
|
||||||
|
let rankingSql = 'order by ';
|
||||||
|
if (favoritesFirst) {
|
||||||
|
rankingSql +=
|
||||||
|
'favorite_features.feature is not null desc, ';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sortBy.startsWith('environment:')) {
|
||||||
|
const [, envName] = sortBy.split(':');
|
||||||
|
rankingSql += this.db
|
||||||
|
.raw(
|
||||||
|
`CASE WHEN feature_environments.environment = ? THEN feature_environments.enabled ELSE NULL END ${validatedSortOrder} NULLS LAST, features.created_at asc, features.name asc`,
|
||||||
|
[envName],
|
||||||
|
)
|
||||||
|
.toString();
|
||||||
|
} else if (sortByMapping[sortBy]) {
|
||||||
|
rankingSql += `${this.db
|
||||||
|
.raw(`?? ${validatedSortOrder}`, [
|
||||||
|
sortByMapping[sortBy],
|
||||||
|
])
|
||||||
|
.toString()}, features.created_at asc, features.name asc`;
|
||||||
|
} else {
|
||||||
|
rankingSql += `features.created_at ${validatedSortOrder}, features.name asc`;
|
||||||
|
}
|
||||||
|
|
||||||
|
query
|
||||||
|
.select(selectColumns)
|
||||||
|
.denseRank('rank', this.db.raw(rankingSql));
|
||||||
|
})
|
||||||
|
.with(
|
||||||
|
'final_ranks',
|
||||||
|
this.db.raw(
|
||||||
|
'select feature_name, row_number() over (order by min(rank)) as final_rank from ranked_features group by feature_name',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.with(
|
||||||
|
'total_features',
|
||||||
|
this.db.raw('select count(*) as total from final_ranks'),
|
||||||
|
)
|
||||||
|
.select('*')
|
||||||
|
.from('ranked_features')
|
||||||
|
.innerJoin(
|
||||||
|
'final_ranks',
|
||||||
|
'ranked_features.feature_name',
|
||||||
|
'final_ranks.feature_name',
|
||||||
|
)
|
||||||
|
.joinRaw('CROSS JOIN total_features')
|
||||||
|
.whereBetween('final_rank', [offset + 1, offset + limit]);
|
||||||
|
|
||||||
|
const rows = await finalQuery;
|
||||||
|
stopTimer();
|
||||||
|
if (rows.length > 0) {
|
||||||
|
const overview = this.getAggregatedSearchData(rows);
|
||||||
|
const features = sortEnvironments(overview);
|
||||||
|
return {
|
||||||
|
features,
|
||||||
|
total: Number(rows[0].total) || 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
features: [],
|
||||||
|
total: 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
getAggregatedSearchData(rows): IFeatureOverview {
|
||||||
|
return rows.reduce((acc, row) => {
|
||||||
|
if (acc[row.feature_name] !== undefined) {
|
||||||
|
const environmentExists = acc[
|
||||||
|
row.feature_name
|
||||||
|
].environments.some(
|
||||||
|
(existingEnvironment) =>
|
||||||
|
existingEnvironment.name === row.environment,
|
||||||
|
);
|
||||||
|
if (!environmentExists) {
|
||||||
|
acc[row.feature_name].environments.push(
|
||||||
|
FeatureSearchStore.getEnvironment(row),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const segmentExists = acc[row.feature_name].segments.includes(
|
||||||
|
row.segment_name,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (row.segment_name && !segmentExists) {
|
||||||
|
acc[row.feature_name].segments.push(row.segment_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.isNewTag(acc[row.feature_name], row)) {
|
||||||
|
this.addTag(acc[row.feature_name], row);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
acc[row.feature_name] = {
|
||||||
|
type: row.type,
|
||||||
|
description: row.description,
|
||||||
|
project: row.project,
|
||||||
|
favorite: row.favorite,
|
||||||
|
name: row.feature_name,
|
||||||
|
createdAt: row.created_at,
|
||||||
|
stale: row.stale,
|
||||||
|
impressionData: row.impression_data,
|
||||||
|
lastSeenAt: row.last_seen_at,
|
||||||
|
environments: [FeatureSearchStore.getEnvironment(row)],
|
||||||
|
segments: row.segment_name ? [row.segment_name] : [],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (this.isNewTag(acc[row.feature_name], row)) {
|
||||||
|
this.addTag(acc[row.feature_name], row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const featureRow = acc[row.feature_name];
|
||||||
|
if (
|
||||||
|
featureRow.lastSeenAt === undefined ||
|
||||||
|
new Date(row.env_last_seen_at) >
|
||||||
|
new Date(featureRow.last_seen_at)
|
||||||
|
) {
|
||||||
|
featureRow.lastSeenAt = row.env_last_seen_at;
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
private addTag(
|
||||||
|
featureToggle: Record<string, any>,
|
||||||
|
row: Record<string, any>,
|
||||||
|
): void {
|
||||||
|
const tags = featureToggle.tags || [];
|
||||||
|
const newTag = this.rowToTag(row);
|
||||||
|
featureToggle.tags = [...tags, newTag];
|
||||||
|
}
|
||||||
|
|
||||||
|
private rowToTag(r: any): ITag {
|
||||||
|
return {
|
||||||
|
value: r.tag_value,
|
||||||
|
type: r.tag_type,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private isNewTag(
|
||||||
|
featureToggle: Record<string, any>,
|
||||||
|
row: Record<string, any>,
|
||||||
|
): boolean {
|
||||||
|
return (
|
||||||
|
row.tag_type &&
|
||||||
|
row.tag_value &&
|
||||||
|
!featureToggle.tags?.some(
|
||||||
|
(tag) =>
|
||||||
|
tag.type === row.tag_type && tag.value === row.tag_value,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const applyQueryParams = (
|
||||||
|
query: Knex.QueryBuilder,
|
||||||
|
queryParams: IQueryParam[],
|
||||||
|
): void => {
|
||||||
|
const tagConditions = queryParams.filter((param) => param.field === 'tag');
|
||||||
|
const segmentConditions = queryParams.filter(
|
||||||
|
(param) => param.field === 'segment',
|
||||||
|
);
|
||||||
|
const genericConditions = queryParams.filter(
|
||||||
|
(param) => param.field !== 'tag',
|
||||||
|
);
|
||||||
|
applyGenericQueryParams(query, genericConditions);
|
||||||
|
|
||||||
|
applyMultiQueryParams(
|
||||||
|
query,
|
||||||
|
tagConditions,
|
||||||
|
['tag_type', 'tag_value'],
|
||||||
|
createTagBaseQuery,
|
||||||
|
);
|
||||||
|
applyMultiQueryParams(
|
||||||
|
query,
|
||||||
|
segmentConditions,
|
||||||
|
'segments.name',
|
||||||
|
createSegmentBaseQuery,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyGenericQueryParams = (
|
||||||
|
query: Knex.QueryBuilder,
|
||||||
|
queryParams: IQueryParam[],
|
||||||
|
): void => {
|
||||||
|
queryParams.forEach((param) => {
|
||||||
|
switch (param.operator) {
|
||||||
|
case 'IS':
|
||||||
|
case 'IS_ANY_OF':
|
||||||
|
query.whereIn(param.field, param.values);
|
||||||
|
break;
|
||||||
|
case 'IS_NOT':
|
||||||
|
case 'IS_NONE_OF':
|
||||||
|
query.whereNotIn(param.field, param.values);
|
||||||
|
break;
|
||||||
|
case 'IS_BEFORE':
|
||||||
|
query.where(param.field, '<', param.values[0]);
|
||||||
|
break;
|
||||||
|
case 'IS_ON_OR_AFTER':
|
||||||
|
query.where(param.field, '>=', param.values[0]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const applyMultiQueryParams = (
|
||||||
|
query: Knex.QueryBuilder,
|
||||||
|
queryParams: IQueryParam[],
|
||||||
|
fields: string | string[],
|
||||||
|
createBaseQuery: (
|
||||||
|
values: string[] | string[][],
|
||||||
|
) => (dbSubQuery: Knex.QueryBuilder) => Knex.QueryBuilder,
|
||||||
|
): void => {
|
||||||
|
queryParams.forEach((param) => {
|
||||||
|
const values = param.values.map((val) =>
|
||||||
|
(Array.isArray(fields) ? val.split(':') : [val]).map((s) =>
|
||||||
|
s.trim(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const baseSubQuery = createBaseQuery(values);
|
||||||
|
|
||||||
|
switch (param.operator) {
|
||||||
|
case 'INCLUDE':
|
||||||
|
case 'INCLUDE_ANY_OF':
|
||||||
|
if (Array.isArray(fields)) {
|
||||||
|
query.whereIn(fields, values);
|
||||||
|
} else {
|
||||||
|
query.whereIn(
|
||||||
|
fields,
|
||||||
|
values.map((v) => v[0]),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'DO_NOT_INCLUDE':
|
||||||
|
case 'EXCLUDE_IF_ANY_OF':
|
||||||
|
query.whereNotIn('features.name', baseSubQuery);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'INCLUDE_ALL_OF':
|
||||||
|
query.whereIn('features.name', (dbSubQuery) => {
|
||||||
|
baseSubQuery(dbSubQuery)
|
||||||
|
.groupBy('feature_name')
|
||||||
|
.havingRaw('COUNT(*) = ?', [values.length]);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'EXCLUDE_ALL':
|
||||||
|
query.whereNotIn('features.name', (dbSubQuery) => {
|
||||||
|
baseSubQuery(dbSubQuery)
|
||||||
|
.groupBy('feature_name')
|
||||||
|
.havingRaw('COUNT(*) = ?', [values.length]);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const createTagBaseQuery = (tags: string[][]) => {
|
||||||
|
return (dbSubQuery: Knex.QueryBuilder): Knex.QueryBuilder => {
|
||||||
|
return dbSubQuery
|
||||||
|
.from('feature_tag')
|
||||||
|
.select('feature_name')
|
||||||
|
.whereIn(['tag_type', 'tag_value'], tags);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const createSegmentBaseQuery = (segments: string[]) => {
|
||||||
|
return (dbSubQuery: Knex.QueryBuilder): Knex.QueryBuilder => {
|
||||||
|
return dbSubQuery
|
||||||
|
.from('feature_strategies')
|
||||||
|
.leftJoin(
|
||||||
|
'feature_strategy_segment',
|
||||||
|
'feature_strategy_segment.feature_strategy_id',
|
||||||
|
'feature_strategies.id',
|
||||||
|
)
|
||||||
|
.leftJoin(
|
||||||
|
'segments',
|
||||||
|
'feature_strategy_segment.segment_id',
|
||||||
|
'segments.id',
|
||||||
|
)
|
||||||
|
.select('feature_name')
|
||||||
|
.whereIn('name', segments);
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = FeatureSearchStore;
|
||||||
|
export default FeatureSearchStore;
|
@ -8,10 +8,7 @@ import {
|
|||||||
FeatureToggle,
|
FeatureToggle,
|
||||||
} from '../../../types/model';
|
} from '../../../types/model';
|
||||||
import NotFoundError from '../../../error/notfound-error';
|
import NotFoundError from '../../../error/notfound-error';
|
||||||
import {
|
import { IFeatureStrategiesStore } from '../types/feature-toggle-strategies-store-type';
|
||||||
IFeatureSearchParams,
|
|
||||||
IFeatureStrategiesStore,
|
|
||||||
} from '../types/feature-toggle-strategies-store-type';
|
|
||||||
import { IFeatureProjectUserParams } from '../feature-toggle-controller';
|
import { IFeatureProjectUserParams } from '../feature-toggle-controller';
|
||||||
|
|
||||||
interface ProjectEnvironment {
|
interface ProjectEnvironment {
|
||||||
@ -324,13 +321,6 @@ export default class FakeFeatureStrategiesStore
|
|||||||
): Promise<IFeatureOverview[]> {
|
): Promise<IFeatureOverview[]> {
|
||||||
return Promise.resolve([]);
|
return Promise.resolve([]);
|
||||||
}
|
}
|
||||||
|
|
||||||
searchFeatures(
|
|
||||||
params: IFeatureSearchParams,
|
|
||||||
): Promise<{ features: IFeatureOverview[]; total: number }> {
|
|
||||||
return Promise.resolve({ features: [], total: 0 });
|
|
||||||
}
|
|
||||||
|
|
||||||
getAllByFeatures(
|
getAllByFeatures(
|
||||||
features: string[],
|
features: string[],
|
||||||
environment?: string,
|
environment?: string,
|
||||||
|
@ -25,10 +25,6 @@ import { ensureStringValue, mapValues } from '../../util';
|
|||||||
import { IFeatureProjectUserParams } from './feature-toggle-controller';
|
import { IFeatureProjectUserParams } from './feature-toggle-controller';
|
||||||
import { Db } from '../../db/db';
|
import { Db } from '../../db/db';
|
||||||
import Raw = Knex.Raw;
|
import Raw = Knex.Raw;
|
||||||
import {
|
|
||||||
IFeatureSearchParams,
|
|
||||||
IQueryParam,
|
|
||||||
} from './types/feature-toggle-strategies-store-type';
|
|
||||||
|
|
||||||
const COLUMNS = [
|
const COLUMNS = [
|
||||||
'id',
|
'id',
|
||||||
@ -527,237 +523,6 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async searchFeatures(
|
|
||||||
{
|
|
||||||
userId,
|
|
||||||
searchParams,
|
|
||||||
type,
|
|
||||||
tag,
|
|
||||||
status,
|
|
||||||
offset,
|
|
||||||
limit,
|
|
||||||
sortOrder,
|
|
||||||
sortBy,
|
|
||||||
favoritesFirst,
|
|
||||||
}: IFeatureSearchParams,
|
|
||||||
queryParams: IQueryParam[],
|
|
||||||
): Promise<{
|
|
||||||
features: IFeatureOverview[];
|
|
||||||
total: number;
|
|
||||||
}> {
|
|
||||||
const validatedSortOrder =
|
|
||||||
sortOrder === 'asc' || sortOrder === 'desc' ? sortOrder : 'asc';
|
|
||||||
|
|
||||||
const finalQuery = this.db
|
|
||||||
.with('ranked_features', (query) => {
|
|
||||||
query.from('features');
|
|
||||||
|
|
||||||
applyQueryParams(query, queryParams);
|
|
||||||
|
|
||||||
const hasSearchParams = searchParams?.length;
|
|
||||||
if (hasSearchParams) {
|
|
||||||
const sqlParameters = searchParams.map(
|
|
||||||
(item) => `%${item}%`,
|
|
||||||
);
|
|
||||||
const sqlQueryParameters = sqlParameters
|
|
||||||
.map(() => '?')
|
|
||||||
.join(',');
|
|
||||||
|
|
||||||
query.where((builder) => {
|
|
||||||
builder
|
|
||||||
.orWhereRaw(
|
|
||||||
`(??) ILIKE ANY (ARRAY[${sqlQueryParameters}])`,
|
|
||||||
['features.name', ...sqlParameters],
|
|
||||||
)
|
|
||||||
.orWhereRaw(
|
|
||||||
`(??) ILIKE ANY (ARRAY[${sqlQueryParameters}])`,
|
|
||||||
['features.description', ...sqlParameters],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (type) {
|
|
||||||
query.whereIn('features.type', type);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (status && status.length > 0) {
|
|
||||||
query.where((builder) => {
|
|
||||||
for (const [envName, envStatus] of status) {
|
|
||||||
builder.orWhere(function () {
|
|
||||||
this.where(
|
|
||||||
'feature_environments.environment',
|
|
||||||
envName,
|
|
||||||
).andWhere(
|
|
||||||
'feature_environments.enabled',
|
|
||||||
envStatus === 'enabled' ? true : false,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
query
|
|
||||||
.modify(FeatureToggleStore.filterByArchived, false)
|
|
||||||
.leftJoin(
|
|
||||||
'feature_environments',
|
|
||||||
'feature_environments.feature_name',
|
|
||||||
'features.name',
|
|
||||||
)
|
|
||||||
.leftJoin(
|
|
||||||
'environments',
|
|
||||||
'feature_environments.environment',
|
|
||||||
'environments.name',
|
|
||||||
)
|
|
||||||
.leftJoin(
|
|
||||||
'feature_tag as ft',
|
|
||||||
'ft.feature_name',
|
|
||||||
'features.name',
|
|
||||||
)
|
|
||||||
.leftJoin(
|
|
||||||
'feature_strategies',
|
|
||||||
'feature_strategies.feature_name',
|
|
||||||
'features.name',
|
|
||||||
)
|
|
||||||
.leftJoin(
|
|
||||||
'feature_strategy_segment',
|
|
||||||
'feature_strategy_segment.feature_strategy_id',
|
|
||||||
'feature_strategies.id',
|
|
||||||
)
|
|
||||||
.leftJoin(
|
|
||||||
'segments',
|
|
||||||
'feature_strategy_segment.segment_id',
|
|
||||||
'segments.id',
|
|
||||||
);
|
|
||||||
|
|
||||||
query.leftJoin('last_seen_at_metrics', function () {
|
|
||||||
this.on(
|
|
||||||
'last_seen_at_metrics.environment',
|
|
||||||
'=',
|
|
||||||
'environments.name',
|
|
||||||
).andOn(
|
|
||||||
'last_seen_at_metrics.feature_name',
|
|
||||||
'=',
|
|
||||||
'features.name',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
let selectColumns = [
|
|
||||||
'features.name as feature_name',
|
|
||||||
'features.description as description',
|
|
||||||
'features.type as type',
|
|
||||||
'features.project as project',
|
|
||||||
'features.created_at as created_at',
|
|
||||||
'features.stale as stale',
|
|
||||||
'features.last_seen_at as last_seen_at',
|
|
||||||
'features.impression_data as impression_data',
|
|
||||||
'feature_environments.enabled as enabled',
|
|
||||||
'feature_environments.environment as environment',
|
|
||||||
'feature_environments.variants as variants',
|
|
||||||
'environments.type as environment_type',
|
|
||||||
'environments.sort_order as environment_sort_order',
|
|
||||||
'ft.tag_value as tag_value',
|
|
||||||
'ft.tag_type as tag_type',
|
|
||||||
'segments.name as segment_name',
|
|
||||||
] as (string | Raw<any> | Knex.QueryBuilder)[];
|
|
||||||
|
|
||||||
const lastSeenQuery = 'last_seen_at_metrics.last_seen_at';
|
|
||||||
selectColumns.push(`${lastSeenQuery} as env_last_seen_at`);
|
|
||||||
|
|
||||||
if (userId) {
|
|
||||||
query.leftJoin(`favorite_features`, function () {
|
|
||||||
this.on(
|
|
||||||
'favorite_features.feature',
|
|
||||||
'features.name',
|
|
||||||
).andOnVal('favorite_features.user_id', '=', userId);
|
|
||||||
});
|
|
||||||
selectColumns = [
|
|
||||||
...selectColumns,
|
|
||||||
this.db.raw(
|
|
||||||
'favorite_features.feature is not null as favorite',
|
|
||||||
),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
selectColumns = [
|
|
||||||
...selectColumns,
|
|
||||||
this.db.raw(
|
|
||||||
'EXISTS (SELECT 1 FROM feature_strategies WHERE feature_strategies.feature_name = features.name AND feature_strategies.environment = feature_environments.environment) as has_strategies',
|
|
||||||
),
|
|
||||||
this.db.raw(
|
|
||||||
'EXISTS (SELECT 1 FROM feature_strategies WHERE feature_strategies.feature_name = features.name AND feature_strategies.environment = feature_environments.environment AND (feature_strategies.disabled IS NULL OR feature_strategies.disabled = false)) as has_enabled_strategies',
|
|
||||||
),
|
|
||||||
];
|
|
||||||
|
|
||||||
const sortByMapping = {
|
|
||||||
name: 'features.name',
|
|
||||||
type: 'features.type',
|
|
||||||
lastSeenAt: lastSeenQuery,
|
|
||||||
};
|
|
||||||
|
|
||||||
let rankingSql = 'order by ';
|
|
||||||
if (favoritesFirst) {
|
|
||||||
rankingSql +=
|
|
||||||
'favorite_features.feature is not null desc, ';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sortBy.startsWith('environment:')) {
|
|
||||||
const [, envName] = sortBy.split(':');
|
|
||||||
rankingSql += this.db
|
|
||||||
.raw(
|
|
||||||
`CASE WHEN feature_environments.environment = ? THEN feature_environments.enabled ELSE NULL END ${validatedSortOrder} NULLS LAST, features.created_at asc, features.name asc`,
|
|
||||||
[envName],
|
|
||||||
)
|
|
||||||
.toString();
|
|
||||||
} else if (sortByMapping[sortBy]) {
|
|
||||||
rankingSql += `${this.db
|
|
||||||
.raw(`?? ${validatedSortOrder}`, [
|
|
||||||
sortByMapping[sortBy],
|
|
||||||
])
|
|
||||||
.toString()}, features.created_at asc, features.name asc`;
|
|
||||||
} else {
|
|
||||||
rankingSql += `features.created_at ${validatedSortOrder}, features.name asc`;
|
|
||||||
}
|
|
||||||
|
|
||||||
query
|
|
||||||
.select(selectColumns)
|
|
||||||
.denseRank('rank', this.db.raw(rankingSql));
|
|
||||||
})
|
|
||||||
.with(
|
|
||||||
'final_ranks',
|
|
||||||
this.db.raw(
|
|
||||||
'select feature_name, row_number() over (order by min(rank)) as final_rank from ranked_features group by feature_name',
|
|
||||||
),
|
|
||||||
)
|
|
||||||
.with(
|
|
||||||
'total_features',
|
|
||||||
this.db.raw('select count(*) as total from final_ranks'),
|
|
||||||
)
|
|
||||||
.select('*')
|
|
||||||
.from('ranked_features')
|
|
||||||
.innerJoin(
|
|
||||||
'final_ranks',
|
|
||||||
'ranked_features.feature_name',
|
|
||||||
'final_ranks.feature_name',
|
|
||||||
)
|
|
||||||
.joinRaw('CROSS JOIN total_features')
|
|
||||||
.whereBetween('final_rank', [offset + 1, offset + limit]);
|
|
||||||
|
|
||||||
const rows = await finalQuery;
|
|
||||||
|
|
||||||
if (rows.length > 0) {
|
|
||||||
const overview = this.getAggregatedSearchData(rows);
|
|
||||||
const features = sortEnvironments(overview);
|
|
||||||
return {
|
|
||||||
features,
|
|
||||||
total: Number(rows[0].total) || 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
features: [],
|
|
||||||
total: 0,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async getFeatureOverview({
|
async getFeatureOverview({
|
||||||
projectId,
|
projectId,
|
||||||
archived,
|
archived,
|
||||||
@ -1090,138 +855,5 @@ class FeatureStrategiesStore implements IFeatureStrategiesStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const applyQueryParams = (
|
|
||||||
query: Knex.QueryBuilder,
|
|
||||||
queryParams: IQueryParam[],
|
|
||||||
): void => {
|
|
||||||
const tagConditions = queryParams.filter((param) => param.field === 'tag');
|
|
||||||
const segmentConditions = queryParams.filter(
|
|
||||||
(param) => param.field === 'segment',
|
|
||||||
);
|
|
||||||
const genericConditions = queryParams.filter(
|
|
||||||
(param) => param.field !== 'tag',
|
|
||||||
);
|
|
||||||
applyGenericQueryParams(query, genericConditions);
|
|
||||||
|
|
||||||
applyMultiQueryParams(
|
|
||||||
query,
|
|
||||||
tagConditions,
|
|
||||||
['tag_type', 'tag_value'],
|
|
||||||
createTagBaseQuery,
|
|
||||||
);
|
|
||||||
applyMultiQueryParams(
|
|
||||||
query,
|
|
||||||
segmentConditions,
|
|
||||||
'segments.name',
|
|
||||||
createSegmentBaseQuery,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const applyGenericQueryParams = (
|
|
||||||
query: Knex.QueryBuilder,
|
|
||||||
queryParams: IQueryParam[],
|
|
||||||
): void => {
|
|
||||||
queryParams.forEach((param) => {
|
|
||||||
switch (param.operator) {
|
|
||||||
case 'IS':
|
|
||||||
case 'IS_ANY_OF':
|
|
||||||
query.whereIn(param.field, param.values);
|
|
||||||
break;
|
|
||||||
case 'IS_NOT':
|
|
||||||
case 'IS_NONE_OF':
|
|
||||||
query.whereNotIn(param.field, param.values);
|
|
||||||
break;
|
|
||||||
case 'IS_BEFORE':
|
|
||||||
query.where(param.field, '<', param.values[0]);
|
|
||||||
break;
|
|
||||||
case 'IS_ON_OR_AFTER':
|
|
||||||
query.where(param.field, '>=', param.values[0]);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const applyMultiQueryParams = (
|
|
||||||
query: Knex.QueryBuilder,
|
|
||||||
queryParams: IQueryParam[],
|
|
||||||
fields: string | string[],
|
|
||||||
createBaseQuery: (
|
|
||||||
values: string[] | string[][],
|
|
||||||
) => (dbSubQuery: Knex.QueryBuilder) => Knex.QueryBuilder,
|
|
||||||
): void => {
|
|
||||||
queryParams.forEach((param) => {
|
|
||||||
const values = param.values.map((val) =>
|
|
||||||
(Array.isArray(fields) ? val.split(':') : [val]).map((s) =>
|
|
||||||
s.trim(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const baseSubQuery = createBaseQuery(values);
|
|
||||||
|
|
||||||
switch (param.operator) {
|
|
||||||
case 'INCLUDE':
|
|
||||||
case 'INCLUDE_ANY_OF':
|
|
||||||
if (Array.isArray(fields)) {
|
|
||||||
query.whereIn(fields, values);
|
|
||||||
} else {
|
|
||||||
query.whereIn(
|
|
||||||
fields,
|
|
||||||
values.map((v) => v[0]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'DO_NOT_INCLUDE':
|
|
||||||
case 'EXCLUDE_IF_ANY_OF':
|
|
||||||
query.whereNotIn('features.name', baseSubQuery);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'INCLUDE_ALL_OF':
|
|
||||||
query.whereIn('features.name', (dbSubQuery) => {
|
|
||||||
baseSubQuery(dbSubQuery)
|
|
||||||
.groupBy('feature_name')
|
|
||||||
.havingRaw('COUNT(*) = ?', [values.length]);
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'EXCLUDE_ALL':
|
|
||||||
query.whereNotIn('features.name', (dbSubQuery) => {
|
|
||||||
baseSubQuery(dbSubQuery)
|
|
||||||
.groupBy('feature_name')
|
|
||||||
.havingRaw('COUNT(*) = ?', [values.length]);
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const createTagBaseQuery = (tags: string[][]) => {
|
|
||||||
return (dbSubQuery: Knex.QueryBuilder): Knex.QueryBuilder => {
|
|
||||||
return dbSubQuery
|
|
||||||
.from('feature_tag')
|
|
||||||
.select('feature_name')
|
|
||||||
.whereIn(['tag_type', 'tag_value'], tags);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const createSegmentBaseQuery = (segments: string[]) => {
|
|
||||||
return (dbSubQuery: Knex.QueryBuilder): Knex.QueryBuilder => {
|
|
||||||
return dbSubQuery
|
|
||||||
.from('feature_strategies')
|
|
||||||
.leftJoin(
|
|
||||||
'feature_strategy_segment',
|
|
||||||
'feature_strategy_segment.feature_strategy_id',
|
|
||||||
'feature_strategies.id',
|
|
||||||
)
|
|
||||||
.leftJoin(
|
|
||||||
'segments',
|
|
||||||
'feature_strategy_segment.segment_id',
|
|
||||||
'segments.id',
|
|
||||||
)
|
|
||||||
.select('feature_name')
|
|
||||||
.whereIn('name', segments);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = FeatureStrategiesStore;
|
module.exports = FeatureStrategiesStore;
|
||||||
export default FeatureStrategiesStore;
|
export default FeatureStrategiesStore;
|
||||||
|
@ -91,14 +91,6 @@ export interface IFeatureStrategiesStore
|
|||||||
params: IFeatureProjectUserParams,
|
params: IFeatureProjectUserParams,
|
||||||
): Promise<IFeatureOverview[]>;
|
): Promise<IFeatureOverview[]>;
|
||||||
|
|
||||||
searchFeatures(
|
|
||||||
params: IFeatureSearchParams,
|
|
||||||
queryParams: IQueryParam[],
|
|
||||||
): Promise<{
|
|
||||||
features: IFeatureOverview[];
|
|
||||||
total: number;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
getStrategyById(id: string): Promise<IFeatureStrategy>;
|
getStrategyById(id: string): Promise<IFeatureStrategy>;
|
||||||
|
|
||||||
updateStrategy(
|
updateStrategy(
|
||||||
|
@ -36,6 +36,7 @@ import { IImportTogglesStore } from '../features/export-import-toggles/import-to
|
|||||||
import { IPrivateProjectStore } from '../features/private-project/privateProjectStoreType';
|
import { IPrivateProjectStore } from '../features/private-project/privateProjectStoreType';
|
||||||
import { IDependentFeaturesStore } from '../features/dependent-features/dependent-features-store-type';
|
import { IDependentFeaturesStore } from '../features/dependent-features/dependent-features-store-type';
|
||||||
import { ILastSeenStore } from '../services/client-metrics/last-seen/types/last-seen-store-type';
|
import { ILastSeenStore } from '../services/client-metrics/last-seen/types/last-seen-store-type';
|
||||||
|
import { IFeatureSearchStore } from '../features/feature-search/feature-search-store-type';
|
||||||
|
|
||||||
export interface IUnleashStores {
|
export interface IUnleashStores {
|
||||||
accessStore: IAccessStore;
|
accessStore: IAccessStore;
|
||||||
@ -76,6 +77,7 @@ export interface IUnleashStores {
|
|||||||
privateProjectStore: IPrivateProjectStore;
|
privateProjectStore: IPrivateProjectStore;
|
||||||
dependentFeaturesStore: IDependentFeaturesStore;
|
dependentFeaturesStore: IDependentFeaturesStore;
|
||||||
lastSeenStore: ILastSeenStore;
|
lastSeenStore: ILastSeenStore;
|
||||||
|
featureSearchStore: IFeatureSearchStore;
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@ -116,4 +118,5 @@ export {
|
|||||||
IPrivateProjectStore,
|
IPrivateProjectStore,
|
||||||
IDependentFeaturesStore,
|
IDependentFeaturesStore,
|
||||||
ILastSeenStore,
|
ILastSeenStore,
|
||||||
|
IFeatureSearchStore,
|
||||||
};
|
};
|
||||||
|
2
src/test/fixtures/store.ts
vendored
2
src/test/fixtures/store.ts
vendored
@ -39,6 +39,7 @@ import { FakeAccountStore } from './fake-account-store';
|
|||||||
import FakeProjectStatsStore from './fake-project-stats-store';
|
import FakeProjectStatsStore from './fake-project-stats-store';
|
||||||
import { FakeDependentFeaturesStore } from '../../lib/features/dependent-features/fake-dependent-features-store';
|
import { FakeDependentFeaturesStore } from '../../lib/features/dependent-features/fake-dependent-features-store';
|
||||||
import { FakeLastSeenStore } from '../../lib/services/client-metrics/last-seen/fake-last-seen-store';
|
import { FakeLastSeenStore } from '../../lib/services/client-metrics/last-seen/fake-last-seen-store';
|
||||||
|
import FakeFeatureSearchStore from '../../lib/features/feature-search/fake-feature-search-store';
|
||||||
|
|
||||||
const db = {
|
const db = {
|
||||||
select: () => ({
|
select: () => ({
|
||||||
@ -87,6 +88,7 @@ const createStores: () => IUnleashStores = () => {
|
|||||||
privateProjectStore: {} as IPrivateProjectStore,
|
privateProjectStore: {} as IPrivateProjectStore,
|
||||||
dependentFeaturesStore: new FakeDependentFeaturesStore(),
|
dependentFeaturesStore: new FakeDependentFeaturesStore(),
|
||||||
lastSeenStore: new FakeLastSeenStore(),
|
lastSeenStore: new FakeLastSeenStore(),
|
||||||
|
featureSearchStore: new FakeFeatureSearchStore(),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user