1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-01-31 00:16:47 +01:00
unleash.unleash/src/lib/db/client-applications-store.ts

456 lines
14 KiB
TypeScript
Raw Normal View History

import type EventEmitter from 'events';
import NotFoundError from '../error/notfound-error';
import type {
IClientApplication,
IClientApplications,
IClientApplicationsSearchParams,
IClientApplicationsStore,
} from '../types/stores/client-applications-store';
import type { Logger, LogProvider } from '../logger';
import type { Db } from './db';
import type { IApplicationOverview } from '../features/metrics/instance/models';
import { applySearchFilters } from '../features/feature-search/search-utils';
import type { IFlagResolver } from '../types';
import metricsHelper from '../util/metrics-helper';
import { DB_TIME } from '../metric-events';
2016-12-06 09:19:15 +01:00
2017-06-28 10:17:14 +02:00
const COLUMNS = [
'app_name',
'created_at',
'created_by',
2017-06-28 10:17:14 +02:00
'updated_at',
'description',
'strategies',
'url',
'color',
'icon',
];
2016-12-06 09:19:15 +01:00
const TABLE = 'client_applications';
const TABLE_USAGE = 'client_applications_usage';
const DEPRECATED_STRATEGIES = [
'gradualRolloutRandom',
'gradualRolloutSessionId',
'gradualRolloutUserId',
];
const mapRow: (any) => IClientApplication = (row) => ({
2016-12-06 09:19:15 +01:00
appName: row.app_name,
createdAt: row.created_at,
updatedAt: row.updated_at,
description: row.description,
strategies: row.strategies || [],
createdBy: row.created_by,
2016-12-06 09:19:15 +01:00
url: row.url,
color: row.color,
icon: row.icon,
lastSeen: row.last_seen,
announced: row.announced,
project: row.project,
environment: row.environment,
2016-12-06 09:19:15 +01:00
});
const reduceRows = (rows: any[]): IClientApplication[] => {
const appsObj = rows.reduce((acc, row) => {
// extracting project and environment from usage table
const { project, environment } = row;
const existingApp = acc[row.app_name];
if (existingApp) {
const existingProject = existingApp.usage.find(
(usage) => usage.project === project,
);
if (existingProject) {
existingProject.environments.push(environment);
} else {
existingApp.usage.push({
project: project,
environments: [environment],
});
}
} else {
acc[row.app_name] = {
...mapRow(row),
usage:
project && environment
? [
{
project,
environments: [environment],
},
]
: [],
};
}
return acc;
}, {});
return Object.values(appsObj);
};
const remapRow = (input) => {
const temp = {
app_name: input.appName,
updated_at: input.updatedAt || new Date(),
seen_at: input.lastSeen || new Date(),
description: input.description,
created_by: input.createdBy,
announced: input.announced,
url: input.url,
color: input.color,
icon: input.icon,
strategies: JSON.stringify(input.strategies),
};
Object.keys(temp).forEach((k) => {
if (temp[k] === undefined) {
// not using !temp[k] to allow false and null values to get through
delete temp[k];
}
});
return temp;
};
2016-12-06 09:19:15 +01:00
chore(deps): update dependency @biomejs/biome to v1.4.1 (#5709) [![Mend Renovate](https://app.renovatebot.com/images/banner.svg)](https://renovatebot.com) This PR contains the following updates: | Package | Change | Age | Adoption | Passing | Confidence | |---|---|---|---|---|---| | [@biomejs/biome](https://biomejs.dev) ([source](https://togithub.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome)) | [`1.4.0` -> `1.4.1`](https://renovatebot.com/diffs/npm/@biomejs%2fbiome/1.4.0/1.4.1) | [![age](https://developer.mend.io/api/mc/badges/age/npm/@biomejs%2fbiome/1.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![adoption](https://developer.mend.io/api/mc/badges/adoption/npm/@biomejs%2fbiome/1.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![passing](https://developer.mend.io/api/mc/badges/compatibility/npm/@biomejs%2fbiome/1.4.0/1.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | [![confidence](https://developer.mend.io/api/mc/badges/confidence/npm/@biomejs%2fbiome/1.4.0/1.4.1?slim=true)](https://docs.renovatebot.com/merge-confidence/) | --- ### Release Notes <details> <summary>biomejs/biome (@&#8203;biomejs/biome)</summary> ### [`v1.4.1`](https://togithub.com/biomejs/biome/blob/HEAD/CHANGELOG.md#141-2023-11-30) [Compare Source](https://togithub.com/biomejs/biome/compare/889593e3f983a6fec642d20eea3c7f94d58fc7e1...a88751306242058374575b9f511e3c22213032b6) ##### Editors - Fix [#&#8203;933](https://togithub.com/biomejs/biome/issues/933). Some files are properly ignored in the LSP too. E.g. `package.json`, `tsconfig.json`, etc. ##### Formatter ##### Bug fixes - Fix some accidental line breaks when printing array expressions within arrow functions and other long lines [#&#8203;917](https://togithub.com/biomejs/biome/pull/917). Contributed by [@&#8203;faultyserver](https://togithub.com/faultyserver) - Match Prettier's breaking strategy for `ArrowChain` layouts [#&#8203;934](https://togithub.com/biomejs/biome/pull/934). Contributed by [@&#8203;faultyserver](https://togithub.com/faultyserver) - Fix double-printing of leading comments in arrow chain expressions [#&#8203;951](https://togithub.com/biomejs/biome/pull/951). Contributed by [@&#8203;faultyserver](https://togithub.com/faultyserver) ##### Linter ##### Bug fixes - Fix [#&#8203;910](https://togithub.com/biomejs/biome/issues/910), where the rule `noSvgWithoutTitle` should skip elements that have `aria-hidden` attributes. Contributed by [@&#8203;vasucp1207](https://togithub.com/vasucp1207) ##### New features - Add [useForOf](https://biomejs.dev/linter/rules/use-for-of) rule. The rule recommends a for-of loop when the loop index is only used to read from an array that is being iterated. Contributed by [@&#8203;victor-teles](https://togithub.com/victor-teles) ##### Enhancement - Implements [#&#8203;924](https://togithub.com/biomejs/biome/issues/924) and [#&#8203;920](https://togithub.com/biomejs/biome/issues/920). [noUselessElse](https://biomejs.dev/linter/rules/no-useless-else) now ignores `else` clauses that follow at least one `if` statement that doesn't break early. Contributed by [@&#8203;Conaclos](https://togithub.com/Conaclos) For example, the following code is no longer reported by the rule: ```js function f(x) { if (x < 0) { // this `if` doesn't break early. } else if (x > 0) { return x; } else { // This `else` block was previously reported as useless. } } ``` ##### Bug fixes - Fix [#&#8203;918](https://togithub.com/biomejs/biome/issues/918), [useSimpleNumberKeys](https://biomejs.dev/linter/rules/use-simple-number-keys) no longer repports false positive on comments. Contributed by [@&#8203;kalleep](https://togithub.com/kalleep) - Fix [#&#8203;953](https://togithub.com/biomejs/biome/issues/953), [noRedeclare](https://biomejs.dev/linter/rules/no-redeclare) no longer reports type parameters with the same name in different mapped types as redeclarations. Contributed by [@&#8203;Conaclos](https://togithub.com/Conaclos) - Fix [#&#8203;608](https://togithub.com/biomejs/biome/issues/608), [useExhaustiveDependencies](https://biomejs.dev/linter/rules/use-exhaustive-dependencies) no longer repports missing dependencies for React hooks without dependency array. Contributed by [@&#8203;kalleep](https://togithub.com/kalleep) ##### Parser </details> --- ### Configuration 📅 **Schedule**: Branch creation - "after 7pm every weekday,before 5am every weekday" in timezone Europe/Madrid, Automerge - At any time (no schedule defined). 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://www.mend.io/free-developer-tools/renovate/). View repository job log [here](https://developer.mend.io/github/Unleash/unleash). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzNy4xMDMuMSIsInVwZGF0ZWRJblZlciI6IjM3LjEyNy4wIiwidGFyZ2V0QnJhbmNoIjoibWFpbiJ9--> --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: David Leek <david@getunleash.io>
2024-01-10 10:11:49 +01:00
export default class ClientApplicationsStore
implements IClientApplicationsStore
{
private db: Db;
private logger: Logger;
private timer: Function;
private flagResolver: IFlagResolver;
constructor(
db: Db,
eventBus: EventEmitter,
getLogger: LogProvider,
flagResolver: IFlagResolver,
) {
2016-12-06 09:19:15 +01:00
this.db = db;
this.flagResolver = flagResolver;
this.logger = getLogger('client-applications-store.ts');
this.timer = (action: string) =>
metricsHelper.wrapTimer(eventBus, DB_TIME, {
store: 'client-applications',
action,
});
2016-12-06 09:19:15 +01:00
}
async upsert(details: Partial<IClientApplication>): Promise<void> {
const row = remapRow(details);
await this.db(TABLE).insert(row).onConflict('app_name').merge();
const usageRows = this.remapUsageRow(details);
await this.db(TABLE_USAGE)
.insert(usageRows)
.onConflict(['app_name', 'project', 'environment'])
.merge();
2016-12-06 09:19:15 +01:00
}
async bulkUpsert(apps: Partial<IClientApplication>[]): Promise<void> {
const rows = apps.map(remapRow);
const usageRows = apps.flatMap(this.remapUsageRow);
await this.db(TABLE).insert(rows).onConflict('app_name').merge();
await this.db(TABLE_USAGE)
.insert(usageRows)
.onConflict(['app_name', 'project', 'environment'])
.merge();
2016-12-06 09:19:15 +01:00
}
async exists(appName: string): Promise<boolean> {
const result = await this.db.raw(
`SELECT EXISTS(SELECT 1 FROM ${TABLE} WHERE app_name = ?) AS present`,
[appName],
);
const { present } = result.rows[0];
return present;
2016-12-06 09:19:15 +01:00
}
async getAll(): Promise<IClientApplication[]> {
2020-09-18 09:05:09 +02:00
const rows = await this.db
2017-11-02 09:23:38 +01:00
.select(COLUMNS)
.from(TABLE)
2020-09-18 09:05:09 +02:00
.orderBy('app_name', 'asc');
return rows.map(mapRow);
}
async getApplication(appName: string): Promise<IClientApplication> {
2020-09-18 09:05:09 +02:00
const row = await this.db
2017-06-28 10:17:14 +02:00
.select(COLUMNS)
.where('app_name', appName)
.from(TABLE)
2020-09-18 09:05:09 +02:00
.first();
2021-01-06 13:25:25 +01:00
if (!row) {
throw new NotFoundError(`Could not find appName=${appName}`);
}
2020-09-18 09:05:09 +02:00
return mapRow(row);
}
async deleteApplication(appName: string): Promise<void> {
return this.db(TABLE).where('app_name', appName).del();
2020-09-25 09:39:12 +02:00
}
async getApplications(
params: IClientApplicationsSearchParams,
): Promise<IClientApplications> {
const { limit, offset, sortOrder = 'asc', searchParams } = params;
const validatedSortOrder =
sortOrder === 'asc' || sortOrder === 'desc' ? sortOrder : 'asc';
const query = this.db
.with('applications', (qb) => {
applySearchFilters(qb, searchParams, [
'client_applications.app_name',
]);
qb.select([
...COLUMNS.map((column) => `${TABLE}.${column}`),
'project',
'environment',
this.db.raw(
`DENSE_RANK() OVER (ORDER BY client_applications.app_name ${validatedSortOrder}) AS rank`,
),
])
.from(TABLE)
.leftJoin(
TABLE_USAGE,
`${TABLE_USAGE}.app_name`,
`${TABLE}.app_name`,
);
})
.with(
'final_ranks',
this.db.raw(
'select row_number() over (order by min(rank)) as final_rank from applications group by app_name',
),
)
.with(
'total',
this.db.raw('select count(*) as total from final_ranks'),
)
.select('*')
.from('applications')
.joinRaw('CROSS JOIN total')
.whereBetween('rank', [offset + 1, offset + limit]);
const rows = await query;
if (rows.length !== 0) {
const applications = reduceRows(rows);
return {
applications,
total: Number(rows[0].total) || 0,
};
}
return {
applications: [],
total: 0,
};
}
async getUnannounced(): Promise<IClientApplication[]> {
const rows = await this.db(TABLE)
.select(COLUMNS)
.where('announced', false);
return rows.map(mapRow);
}
/** *
* Updates all rows that have announced = false to announced =true and returns the rows altered
* @return {[app]} - Apps that hadn't been announced
*/
async setUnannouncedToAnnounced(): Promise<IClientApplication[]> {
const rows = await this.db(TABLE)
.update({ announced: true })
.where('announced', false)
.whereNotNull('announced')
.returning(COLUMNS);
return rows.map(mapRow);
}
2016-12-06 09:19:15 +01:00
async delete(key: string): Promise<void> {
await this.db(TABLE).where('app_name', key).del();
}
async deleteAll(): Promise<void> {
await this.db(TABLE).del();
}
destroy(): void {}
async get(appName: string): Promise<IClientApplication> {
const row = await this.db
.select(COLUMNS)
.where('app_name', appName)
.from(TABLE)
.first();
if (!row) {
throw new NotFoundError(`Could not find appName=${appName}`);
}
return mapRow(row);
}
async getApplicationOverview(
appName: string,
): Promise<IApplicationOverview> {
const stopTimer = this.timer('getApplicationOverview');
const query = this.db
.with('metrics', (qb) => {
qb.select([
'cme.app_name',
'cme.environment',
'f.project',
this.db.raw(
'array_agg(DISTINCT cme.feature_name) as features',
),
])
.from('client_metrics_env as cme')
.leftJoin('features as f', 'f.name', 'cme.feature_name')
.groupBy('cme.app_name', 'cme.environment', 'f.project');
})
.select([
'm.project',
'm.environment',
'm.features',
'ci.instance_id',
'ci.sdk_version',
'ci.last_seen',
'a.strategies',
])
.from({ a: 'client_applications' })
.leftJoin('metrics as m', 'm.app_name', 'a.app_name')
.leftJoin('client_instances as ci', function () {
this.on('ci.app_name', '=', 'm.app_name').andOn(
'ci.environment',
'=',
'm.environment',
);
})
.where('a.app_name', appName)
.orderBy('m.environment', 'asc');
const rows = await query;
stopTimer();
if (!rows.length) {
throw new NotFoundError(`Could not find appName=${appName}`);
}
const existingStrategies: string[] = await this.db
.select('name')
.from('strategies')
.pluck('name');
return this.mapApplicationOverviewData(rows, existingStrategies);
}
mapApplicationOverviewData(
rows: any[],
existingStrategies: string[],
): IApplicationOverview {
const featureCount = new Set(rows.flatMap((row) => row.features)).size;
const missingStrategies: Set<string> = new Set();
const environments = rows.reduce((acc, row) => {
const {
environment,
instance_id,
sdk_version,
last_seen,
project,
features,
strategies,
} = row;
if (!environment) return acc;
strategies.forEach((strategy) => {
if (
!DEPRECATED_STRATEGIES.includes(strategy) &&
!existingStrategies.includes(strategy)
) {
missingStrategies.add(strategy);
}
});
const featuresNotMappedToProject = !project;
let env = acc.find((e) => e.name === environment);
if (!env) {
env = {
name: environment,
instanceCount: instance_id ? 1 : 0,
sdks: sdk_version ? [sdk_version] : [],
lastSeen: last_seen,
uniqueInstanceIds: new Set(
instance_id ? [instance_id] : [],
),
issues: {
missingFeatures: featuresNotMappedToProject
? features
: [],
},
};
acc.push(env);
} else {
if (instance_id) {
env.uniqueInstanceIds.add(instance_id);
env.instanceCount = env.uniqueInstanceIds.size;
}
if (featuresNotMappedToProject) {
env.issues.missingFeatures = features;
}
if (sdk_version && !env.sdks.includes(sdk_version)) {
env.sdks.push(sdk_version);
}
if (new Date(last_seen) > new Date(env.lastSeen)) {
env.lastSeen = last_seen;
}
}
return acc;
}, []);
environments.forEach((env) => {
delete env.uniqueInstanceIds;
env.sdks.sort();
});
return {
projects: [
...new Set(
rows
.filter((row) => row.project != null)
.map((row) => row.project),
),
],
featureCount,
environments,
issues: {
missingStrategies: [...missingStrategies],
},
};
}
private remapUsageRow = (input) => {
if (!input.projects || input.projects.length === 0) {
return [
{
app_name: input.appName,
project: '*',
environment: input.environment || '*',
},
];
} else {
return input.projects.map((project) => ({
app_name: input.appName,
project: project,
environment: input.environment || '*',
}));
}
};
}