2021-07-07 10:46:50 +02:00
|
|
|
import { Knex } from 'knex';
|
|
|
|
import EventEmitter from 'events';
|
2023-10-11 09:38:57 +02:00
|
|
|
import metricsHelper from '../../util/metrics-helper';
|
|
|
|
import { DB_TIME } from '../../metric-events';
|
|
|
|
import NotFoundError from '../../error/notfound-error';
|
|
|
|
import { Logger, LogProvider } from '../../logger';
|
2023-10-12 13:58:23 +02:00
|
|
|
import {
|
|
|
|
FeatureToggle,
|
|
|
|
FeatureToggleDTO,
|
|
|
|
IFeatureToggleQuery,
|
|
|
|
IVariant,
|
|
|
|
} from '../../types/model';
|
2023-10-11 09:38:57 +02:00
|
|
|
import { IFeatureToggleStore } from './types/feature-toggle-store-type';
|
|
|
|
import { Db } from '../../db/db';
|
2024-01-17 09:14:31 +01:00
|
|
|
import { LastSeenInput } from '../metrics/last-seen/last-seen-service';
|
2023-10-11 09:38:57 +02:00
|
|
|
import { NameExistsError } from '../../error';
|
2023-10-16 12:29:31 +02:00
|
|
|
import { DEFAULT_ENV } from '../../../lib/util';
|
|
|
|
|
2023-10-13 12:29:14 +02:00
|
|
|
import { FeatureToggleListBuilder } from './query-builders/feature-toggle-list-builder';
|
2023-10-16 12:29:31 +02:00
|
|
|
import { FeatureConfigurationClient } from './types/feature-toggle-strategies-store-type';
|
2024-01-25 13:09:30 +01:00
|
|
|
import {
|
|
|
|
ADMIN_TOKEN_USER,
|
|
|
|
IFeatureTypeCount,
|
|
|
|
IFlagResolver,
|
|
|
|
} from '../../../lib/types';
|
2023-10-16 14:19:46 +02:00
|
|
|
import { FeatureToggleRowConverter } from './converters/feature-toggle-row-converter';
|
2023-12-01 10:20:24 +01:00
|
|
|
import { IFeatureProjectUserParams } from './feature-toggle-controller';
|
2023-08-04 08:59:54 +02:00
|
|
|
|
2023-12-01 10:20:24 +01:00
|
|
|
export type EnvironmentFeatureNames = {
|
|
|
|
[key: string]: string[];
|
|
|
|
};
|
2021-07-07 10:46:50 +02:00
|
|
|
|
|
|
|
const FEATURE_COLUMNS = [
|
|
|
|
'name',
|
|
|
|
'description',
|
|
|
|
'type',
|
|
|
|
'project',
|
|
|
|
'stale',
|
|
|
|
'created_at',
|
2022-02-03 11:06:51 +01:00
|
|
|
'impression_data',
|
2021-07-07 10:46:50 +02:00
|
|
|
'last_seen_at',
|
2022-07-01 13:51:26 +02:00
|
|
|
'archived_at',
|
2021-07-07 10:46:50 +02:00
|
|
|
];
|
|
|
|
|
|
|
|
export interface FeaturesTable {
|
|
|
|
name: string;
|
|
|
|
description: string;
|
|
|
|
type: string;
|
|
|
|
stale: boolean;
|
|
|
|
project: string;
|
|
|
|
last_seen_at?: Date;
|
|
|
|
created_at?: Date;
|
2022-02-03 11:06:51 +01:00
|
|
|
impression_data: boolean;
|
2022-07-01 13:51:26 +02:00
|
|
|
archived?: boolean;
|
|
|
|
archived_at?: Date;
|
2023-12-22 14:33:16 +01:00
|
|
|
created_by_user_id?: number;
|
|
|
|
}
|
|
|
|
|
|
|
|
export interface FeatureToggleInsert
|
|
|
|
extends Omit<FeatureToggleDTO, 'createdByUserId'> {
|
|
|
|
createdByUserId: number;
|
2021-07-07 10:46:50 +02:00
|
|
|
}
|
|
|
|
|
2022-11-21 10:37:16 +01:00
|
|
|
interface VariantDTO {
|
|
|
|
variants: IVariant[];
|
|
|
|
}
|
|
|
|
|
2023-10-20 13:17:41 +02:00
|
|
|
const commonSelectColumns = [
|
|
|
|
'features.name as name',
|
|
|
|
'features.description as description',
|
|
|
|
'features.type as type',
|
|
|
|
'features.project as project',
|
|
|
|
'features.stale as stale',
|
|
|
|
'features.impression_data as impression_data',
|
|
|
|
'features.last_seen_at as last_seen_at',
|
|
|
|
'features.created_at as created_at',
|
|
|
|
];
|
|
|
|
|
2021-07-07 10:46:50 +02:00
|
|
|
const TABLE = 'features';
|
2022-11-21 10:37:16 +01:00
|
|
|
const FEATURE_ENVIRONMENTS_TABLE = 'feature_environments';
|
2021-07-07 10:46:50 +02:00
|
|
|
|
2021-08-12 15:04:37 +02:00
|
|
|
export default class FeatureToggleStore implements IFeatureToggleStore {
|
2023-01-30 09:02:44 +01:00
|
|
|
private db: Db;
|
2021-07-07 10:46:50 +02:00
|
|
|
|
|
|
|
private logger: Logger;
|
|
|
|
|
|
|
|
private timer: Function;
|
|
|
|
|
2023-10-16 14:19:46 +02:00
|
|
|
private featureToggleRowConverter: FeatureToggleRowConverter;
|
|
|
|
|
2023-10-18 16:34:42 +02:00
|
|
|
private flagResolver: IFlagResolver;
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
db: Db,
|
|
|
|
eventBus: EventEmitter,
|
|
|
|
getLogger: LogProvider,
|
|
|
|
flagResolver: IFlagResolver,
|
|
|
|
) {
|
2021-07-07 10:46:50 +02:00
|
|
|
this.db = db;
|
|
|
|
this.logger = getLogger('feature-toggle-store.ts');
|
2023-10-18 16:34:42 +02:00
|
|
|
this.featureToggleRowConverter = new FeatureToggleRowConverter(
|
|
|
|
flagResolver,
|
|
|
|
);
|
2023-10-19 10:58:10 +02:00
|
|
|
this.flagResolver = flagResolver;
|
2021-08-12 15:04:37 +02:00
|
|
|
this.timer = (action) =>
|
2021-07-07 10:46:50 +02:00
|
|
|
metricsHelper.wrapTimer(eventBus, DB_TIME, {
|
|
|
|
store: 'feature-toggle',
|
|
|
|
action,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
async count(
|
|
|
|
query: {
|
|
|
|
archived?: boolean;
|
|
|
|
project?: string;
|
|
|
|
stale?: boolean;
|
|
|
|
} = { archived: false },
|
|
|
|
): Promise<number> {
|
2022-07-01 13:51:26 +02:00
|
|
|
const { archived, ...rest } = query;
|
2021-07-07 10:46:50 +02:00
|
|
|
return this.db
|
|
|
|
.from(TABLE)
|
2022-05-09 15:20:12 +02:00
|
|
|
.count('*')
|
2022-07-01 13:51:26 +02:00
|
|
|
.where(rest)
|
|
|
|
.modify(FeatureToggleStore.filterByArchived, archived)
|
2021-08-12 15:04:37 +02:00
|
|
|
.then((res) => Number(res[0].count));
|
2021-07-07 10:46:50 +02:00
|
|
|
}
|
|
|
|
|
2021-08-12 15:04:37 +02:00
|
|
|
async deleteAll(): Promise<void> {
|
|
|
|
await this.db(TABLE).del();
|
|
|
|
}
|
|
|
|
|
|
|
|
destroy(): void {}
|
|
|
|
|
|
|
|
async get(name: string): Promise<FeatureToggle> {
|
|
|
|
return this.db
|
|
|
|
.first(FEATURE_COLUMNS)
|
|
|
|
.from(TABLE)
|
2021-09-13 10:23:57 +02:00
|
|
|
.where({ name })
|
2021-08-12 15:04:37 +02:00
|
|
|
.then(this.rowToFeature);
|
|
|
|
}
|
|
|
|
|
2023-10-16 12:29:31 +02:00
|
|
|
private getBaseFeatureQuery = (archived: boolean, environment: string) => {
|
2023-10-20 13:17:41 +02:00
|
|
|
const builder = new FeatureToggleListBuilder(this.db, [
|
|
|
|
...commonSelectColumns,
|
|
|
|
'fe.variants as variants',
|
|
|
|
'fe.enabled as enabled',
|
|
|
|
'fe.environment as environment',
|
|
|
|
'fs.id as strategy_id',
|
|
|
|
'fs.strategy_name as strategy_name',
|
|
|
|
'fs.title as strategy_title',
|
|
|
|
'fs.disabled as strategy_disabled',
|
|
|
|
'fs.parameters as parameters',
|
|
|
|
'fs.constraints as constraints',
|
|
|
|
'fs.sort_order as sort_order',
|
|
|
|
'fs.variants as strategy_variants',
|
|
|
|
'segments.id as segment_id',
|
|
|
|
'segments.constraints as segment_constraints',
|
|
|
|
]);
|
2023-10-13 12:29:14 +02:00
|
|
|
|
|
|
|
builder
|
|
|
|
.query('features')
|
|
|
|
.withArchived(archived)
|
|
|
|
.withStrategies(environment)
|
|
|
|
.withFeatureEnvironments(environment)
|
|
|
|
.withFeatureStrategySegments()
|
2023-10-16 12:29:31 +02:00
|
|
|
.withSegments();
|
|
|
|
|
|
|
|
return builder;
|
|
|
|
};
|
|
|
|
|
|
|
|
async getFeatureToggleList(
|
|
|
|
featureQuery?: IFeatureToggleQuery,
|
|
|
|
userId?: number,
|
|
|
|
archived: boolean = false,
|
2023-10-17 11:35:07 +02:00
|
|
|
includeDisabledStrategies: boolean = false,
|
2023-10-16 12:29:31 +02:00
|
|
|
): Promise<FeatureToggle[]> {
|
|
|
|
const environment = featureQuery?.environment || DEFAULT_ENV;
|
|
|
|
|
|
|
|
const builder = this.getBaseFeatureQuery(
|
|
|
|
archived,
|
|
|
|
environment,
|
|
|
|
).withFeatureTags();
|
|
|
|
|
|
|
|
builder.addSelectColumn('ft.tag_value as tag_value');
|
|
|
|
builder.addSelectColumn('ft.tag_type as tag_type');
|
2023-10-12 13:58:23 +02:00
|
|
|
|
2023-11-30 09:17:50 +01:00
|
|
|
builder.withLastSeenByEnvironment(archived);
|
|
|
|
builder.addSelectColumn(
|
|
|
|
'last_seen_at_metrics.last_seen_at as env_last_seen_at',
|
|
|
|
);
|
|
|
|
builder.addSelectColumn(
|
|
|
|
'last_seen_at_metrics.environment as last_seen_at_env',
|
|
|
|
);
|
2023-10-19 10:58:10 +02:00
|
|
|
|
2023-10-12 13:58:23 +02:00
|
|
|
if (userId) {
|
2023-10-13 12:29:14 +02:00
|
|
|
builder.withFavorites(userId);
|
|
|
|
builder.addSelectColumn(
|
2023-10-12 13:58:23 +02:00
|
|
|
this.db.raw(
|
|
|
|
'favorite_features.feature is not null as favorite',
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-10-13 12:29:14 +02:00
|
|
|
const rows = await builder.internalQuery.select(
|
|
|
|
builder.getSelectColumns(),
|
|
|
|
);
|
|
|
|
|
2023-10-16 14:19:46 +02:00
|
|
|
return this.featureToggleRowConverter.buildFeatureToggleListFromRows(
|
|
|
|
rows,
|
|
|
|
featureQuery,
|
2023-10-17 11:35:07 +02:00
|
|
|
includeDisabledStrategies,
|
2023-10-16 14:19:46 +02:00
|
|
|
);
|
2023-10-12 13:58:23 +02:00
|
|
|
}
|
|
|
|
|
2023-10-16 12:29:31 +02:00
|
|
|
async getPlaygroundFeatures(
|
|
|
|
featureQuery: IFeatureToggleQuery,
|
|
|
|
): Promise<FeatureConfigurationClient[]> {
|
|
|
|
const environment = featureQuery?.environment || DEFAULT_ENV;
|
|
|
|
|
|
|
|
const archived = false;
|
|
|
|
const builder = this.getBaseFeatureQuery(archived, environment);
|
|
|
|
|
2023-11-27 14:54:40 +01:00
|
|
|
builder.withDependentFeatureToggles();
|
2023-10-19 10:58:10 +02:00
|
|
|
|
2023-11-27 14:54:40 +01:00
|
|
|
builder.addSelectColumn('df.parent as parent');
|
|
|
|
builder.addSelectColumn('df.variants as parent_variants');
|
|
|
|
builder.addSelectColumn('df.enabled as parent_enabled');
|
2023-10-16 12:29:31 +02:00
|
|
|
|
2023-10-19 14:05:56 +02:00
|
|
|
if (featureQuery?.project) {
|
|
|
|
builder.forProject(featureQuery.project);
|
|
|
|
}
|
|
|
|
|
2023-10-16 12:29:31 +02:00
|
|
|
const rows = await builder.internalQuery.select(
|
|
|
|
builder.getSelectColumns(),
|
|
|
|
);
|
|
|
|
|
2023-10-16 14:19:46 +02:00
|
|
|
return this.featureToggleRowConverter.buildPlaygroundFeaturesFromRows(
|
2023-10-16 12:29:31 +02:00
|
|
|
rows,
|
|
|
|
featureQuery,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-09-13 10:23:57 +02:00
|
|
|
async getAll(
|
|
|
|
query: {
|
|
|
|
archived?: boolean;
|
|
|
|
project?: string;
|
|
|
|
stale?: boolean;
|
|
|
|
} = { archived: false },
|
|
|
|
): Promise<FeatureToggle[]> {
|
2022-07-01 13:51:26 +02:00
|
|
|
const { archived, ...rest } = query;
|
2021-08-12 15:04:37 +02:00
|
|
|
const rows = await this.db
|
|
|
|
.select(FEATURE_COLUMNS)
|
|
|
|
.from(TABLE)
|
2022-07-01 13:51:26 +02:00
|
|
|
.where(rest)
|
|
|
|
.modify(FeatureToggleStore.filterByArchived, archived);
|
2023-10-20 13:17:41 +02:00
|
|
|
|
2021-07-07 10:46:50 +02:00
|
|
|
return rows.map(this.rowToFeature);
|
|
|
|
}
|
|
|
|
|
2023-10-20 13:17:41 +02:00
|
|
|
async getArchivedFeatures(project?: string): Promise<FeatureToggle[]> {
|
|
|
|
const builder = new FeatureToggleListBuilder(this.db, [
|
|
|
|
...commonSelectColumns,
|
|
|
|
'features.archived_at as archived_at',
|
|
|
|
]);
|
|
|
|
|
2023-10-26 13:38:22 +02:00
|
|
|
const archived = true;
|
|
|
|
builder.query('features').withLastSeenByEnvironment(archived);
|
2023-10-20 13:17:41 +02:00
|
|
|
|
|
|
|
builder.addSelectColumn(
|
|
|
|
'last_seen_at_metrics.last_seen_at as env_last_seen_at',
|
|
|
|
);
|
|
|
|
builder.addSelectColumn(
|
|
|
|
'last_seen_at_metrics.environment as last_seen_at_env',
|
|
|
|
);
|
|
|
|
|
2024-01-12 10:25:59 +01:00
|
|
|
let rows: any[];
|
2023-10-20 13:17:41 +02:00
|
|
|
|
|
|
|
if (project) {
|
|
|
|
rows = await builder.internalQuery
|
|
|
|
.select(builder.getSelectColumns())
|
|
|
|
.where({ project })
|
|
|
|
.whereNotNull('archived_at');
|
|
|
|
} else {
|
|
|
|
rows = await builder.internalQuery
|
|
|
|
.select(builder.getSelectColumns())
|
|
|
|
.whereNotNull('archived_at');
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.featureToggleRowConverter.buildArchivedFeatureToggleListFromRows(
|
|
|
|
rows,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2023-12-01 10:20:24 +01:00
|
|
|
async getFeatureTypeCounts({
|
|
|
|
projectId,
|
|
|
|
archived,
|
|
|
|
}: IFeatureProjectUserParams): Promise<IFeatureTypeCount[]> {
|
|
|
|
const query = this.db<FeaturesTable>(TABLE)
|
|
|
|
.select('type')
|
|
|
|
.count('type')
|
|
|
|
.groupBy('type');
|
|
|
|
|
|
|
|
query.where({
|
|
|
|
project: projectId,
|
|
|
|
archived,
|
|
|
|
});
|
|
|
|
|
|
|
|
const result = await query;
|
|
|
|
return result.map((row) => ({
|
|
|
|
type: row.type,
|
|
|
|
count: Number(row.count),
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
2023-01-11 16:00:20 +01:00
|
|
|
async getAllByNames(names: string[]): Promise<FeatureToggle[]> {
|
2023-02-06 15:46:25 +01:00
|
|
|
const query = this.db<FeaturesTable>(TABLE).orderBy('name', 'asc');
|
2023-02-14 13:13:58 +01:00
|
|
|
query.whereIn('name', names);
|
2023-12-19 08:57:10 +01:00
|
|
|
|
2023-01-11 16:00:20 +01:00
|
|
|
const rows = await query;
|
|
|
|
return rows.map(this.rowToFeature);
|
|
|
|
}
|
|
|
|
|
2023-04-06 15:34:08 +02:00
|
|
|
async countByDate(queryModifiers: {
|
2023-01-26 16:13:15 +01:00
|
|
|
archived?: boolean;
|
|
|
|
project?: string;
|
|
|
|
date?: string;
|
|
|
|
range?: string[];
|
|
|
|
dateAccessor: string;
|
2023-04-06 15:34:08 +02:00
|
|
|
}): Promise<number> {
|
2023-01-26 16:13:15 +01:00
|
|
|
const { project, archived, dateAccessor } = queryModifiers;
|
2023-09-29 14:18:21 +02:00
|
|
|
const query = this.db
|
2023-04-06 15:34:08 +02:00
|
|
|
.count()
|
2023-01-26 16:13:15 +01:00
|
|
|
.from(TABLE)
|
|
|
|
.where({ project })
|
|
|
|
.modify(FeatureToggleStore.filterByArchived, archived);
|
|
|
|
|
|
|
|
if (queryModifiers.date) {
|
|
|
|
query.andWhere(dateAccessor, '>=', queryModifiers.date);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (queryModifiers.range && queryModifiers.range.length === 2) {
|
|
|
|
query.andWhereBetween(dateAccessor, [
|
|
|
|
queryModifiers.range[0],
|
|
|
|
queryModifiers.range[1],
|
|
|
|
]);
|
|
|
|
}
|
|
|
|
|
2023-04-06 15:34:08 +02:00
|
|
|
const queryResult = await query.first();
|
|
|
|
return parseInt(queryResult.count || 0);
|
2023-01-26 16:13:15 +01:00
|
|
|
}
|
|
|
|
|
2021-07-07 10:46:50 +02:00
|
|
|
/**
|
|
|
|
* Get projectId from feature filtered by name. Used by Rbac middleware
|
|
|
|
* @deprecated
|
|
|
|
* @param name
|
|
|
|
*/
|
|
|
|
async getProjectId(name: string): Promise<string> {
|
|
|
|
return this.db
|
|
|
|
.first(['project'])
|
|
|
|
.from(TABLE)
|
|
|
|
.where({ name })
|
2021-08-12 15:04:37 +02:00
|
|
|
.then((r) => (r ? r.project : undefined))
|
|
|
|
.catch((e) => {
|
2021-07-07 10:46:50 +02:00
|
|
|
this.logger.error(e);
|
|
|
|
return undefined;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
async exists(name: string): Promise<boolean> {
|
|
|
|
const result = await this.db.raw(
|
2021-08-12 15:04:37 +02:00
|
|
|
'SELECT EXISTS (SELECT 1 FROM features WHERE name = ?) AS present',
|
2021-07-07 10:46:50 +02:00
|
|
|
[name],
|
|
|
|
);
|
|
|
|
const { present } = result.rows[0];
|
|
|
|
return present;
|
|
|
|
}
|
|
|
|
|
2023-08-04 08:59:54 +02:00
|
|
|
async setLastSeen(data: LastSeenInput[]): Promise<void> {
|
2021-07-07 10:46:50 +02:00
|
|
|
const now = new Date();
|
2023-08-04 08:59:54 +02:00
|
|
|
const environmentArrays = this.mapMetricDataToEnvBuckets(data);
|
2021-07-07 10:46:50 +02:00
|
|
|
try {
|
2023-08-04 08:59:54 +02:00
|
|
|
for (const env of Object.keys(environmentArrays)) {
|
2023-09-15 12:02:38 +02:00
|
|
|
const toggleNames = environmentArrays[env].sort();
|
2023-08-04 08:59:54 +02:00
|
|
|
await this.db(FEATURE_ENVIRONMENTS_TABLE)
|
|
|
|
.update({ last_seen_at: now })
|
|
|
|
.where('environment', env)
|
|
|
|
.whereIn(
|
|
|
|
'feature_name',
|
|
|
|
this.db(FEATURE_ENVIRONMENTS_TABLE)
|
|
|
|
.select('feature_name')
|
|
|
|
.whereIn('feature_name', toggleNames)
|
|
|
|
.forUpdate()
|
|
|
|
.skipLocked(),
|
|
|
|
);
|
|
|
|
|
|
|
|
// Updating the toggle's last_seen_at also for backwards compatibility
|
|
|
|
await this.db(TABLE)
|
|
|
|
.update({ last_seen_at: now })
|
|
|
|
.whereIn(
|
|
|
|
'name',
|
|
|
|
this.db(TABLE)
|
|
|
|
.select('name')
|
|
|
|
.whereIn('name', toggleNames)
|
|
|
|
.forUpdate()
|
|
|
|
.skipLocked(),
|
|
|
|
);
|
|
|
|
}
|
2021-07-07 10:46:50 +02:00
|
|
|
} catch (err) {
|
|
|
|
this.logger.error('Could not update lastSeen, error: ', err);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-04 08:59:54 +02:00
|
|
|
private mapMetricDataToEnvBuckets(
|
|
|
|
data: LastSeenInput[],
|
|
|
|
): EnvironmentFeatureNames {
|
|
|
|
return data.reduce(
|
|
|
|
(acc: EnvironmentFeatureNames, feature: LastSeenInput) => {
|
|
|
|
const { environment, featureName } = feature;
|
|
|
|
|
|
|
|
if (!acc[environment]) {
|
|
|
|
acc[environment] = [];
|
|
|
|
}
|
|
|
|
|
|
|
|
acc[environment].push(featureName);
|
|
|
|
|
|
|
|
return acc;
|
|
|
|
},
|
|
|
|
{},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-07-01 13:51:26 +02:00
|
|
|
static filterByArchived: Knex.QueryCallbackWithArgs = (
|
|
|
|
queryBuilder: Knex.QueryBuilder,
|
|
|
|
archived: boolean,
|
|
|
|
) => {
|
|
|
|
return archived
|
|
|
|
? queryBuilder.whereNotNull('archived_at')
|
|
|
|
: queryBuilder.whereNull('archived_at');
|
|
|
|
};
|
|
|
|
|
2021-07-07 10:46:50 +02:00
|
|
|
rowToFeature(row: FeaturesTable): FeatureToggle {
|
|
|
|
if (!row) {
|
|
|
|
throw new NotFoundError('No feature toggle found');
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
name: row.name,
|
|
|
|
description: row.description,
|
|
|
|
type: row.type,
|
|
|
|
project: row.project,
|
|
|
|
stale: row.stale,
|
|
|
|
createdAt: row.created_at,
|
|
|
|
lastSeenAt: row.last_seen_at,
|
2022-02-03 11:06:51 +01:00
|
|
|
impressionData: row.impression_data,
|
2022-07-01 13:51:26 +02:00
|
|
|
archivedAt: row.archived_at,
|
|
|
|
archived: row.archived_at != null,
|
2021-07-07 10:46:50 +02:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2022-11-21 10:37:16 +01:00
|
|
|
rowToEnvVariants(variantRows: VariantDTO[]): IVariant[] {
|
|
|
|
if (!variantRows.length) {
|
|
|
|
return [];
|
2021-11-24 13:08:04 +01:00
|
|
|
}
|
2021-11-26 13:06:36 +01:00
|
|
|
|
2022-11-21 10:37:16 +01:00
|
|
|
const sortedVariants =
|
|
|
|
(variantRows[0].variants as unknown as IVariant[]) || [];
|
|
|
|
sortedVariants.sort((a, b) => a.name.localeCompare(b.name));
|
2021-11-26 13:06:36 +01:00
|
|
|
return sortedVariants;
|
2021-11-24 13:08:04 +01:00
|
|
|
}
|
|
|
|
|
2023-12-22 14:33:16 +01:00
|
|
|
insertToRow(project: string, data: FeatureToggleInsert): FeaturesTable {
|
2021-07-07 10:46:50 +02:00
|
|
|
const row = {
|
|
|
|
name: data.name,
|
|
|
|
description: data.description,
|
|
|
|
type: data.type,
|
|
|
|
project,
|
2022-07-01 13:51:26 +02:00
|
|
|
archived_at: data.archived ? new Date() : null,
|
2021-07-07 10:46:50 +02:00
|
|
|
stale: data.stale,
|
|
|
|
created_at: data.createdAt,
|
2022-02-03 11:06:51 +01:00
|
|
|
impression_data: data.impressionData,
|
2023-12-22 14:33:16 +01:00
|
|
|
created_by_user_id: data.createdByUserId,
|
2021-07-07 10:46:50 +02:00
|
|
|
};
|
|
|
|
if (!row.created_at) {
|
|
|
|
delete row.created_at;
|
|
|
|
}
|
2023-12-22 14:33:16 +01:00
|
|
|
|
2021-07-07 10:46:50 +02:00
|
|
|
return row;
|
|
|
|
}
|
|
|
|
|
2023-12-22 14:33:16 +01:00
|
|
|
dtoToUpdateRow(
|
2021-07-07 10:46:50 +02:00
|
|
|
project: string,
|
|
|
|
data: FeatureToggleDTO,
|
2023-12-22 14:33:16 +01:00
|
|
|
): Omit<FeaturesTable, 'created_by_user_id'> {
|
|
|
|
const row = {
|
|
|
|
name: data.name,
|
|
|
|
description: data.description,
|
|
|
|
type: data.type,
|
|
|
|
project,
|
|
|
|
archived_at: data.archived ? new Date() : null,
|
|
|
|
stale: data.stale,
|
|
|
|
impression_data: data.impressionData,
|
|
|
|
};
|
|
|
|
|
|
|
|
return row;
|
|
|
|
}
|
|
|
|
|
|
|
|
async create(
|
|
|
|
project: string,
|
|
|
|
data: FeatureToggleInsert,
|
2021-07-07 10:46:50 +02:00
|
|
|
): Promise<FeatureToggle> {
|
|
|
|
try {
|
|
|
|
const row = await this.db(TABLE)
|
2023-12-22 14:33:16 +01:00
|
|
|
.insert(this.insertToRow(project, data))
|
2021-07-07 10:46:50 +02:00
|
|
|
.returning(FEATURE_COLUMNS);
|
|
|
|
|
|
|
|
return this.rowToFeature(row[0]);
|
|
|
|
} catch (err) {
|
|
|
|
this.logger.error('Could not insert feature, error: ', err);
|
2023-08-23 11:11:16 +02:00
|
|
|
if (
|
|
|
|
typeof err.detail === 'string' &&
|
|
|
|
err.detail.includes('already exists')
|
|
|
|
) {
|
|
|
|
throw new NameExistsError(
|
|
|
|
`Feature ${data.name} already exists`,
|
|
|
|
);
|
|
|
|
}
|
|
|
|
throw err;
|
2021-07-07 10:46:50 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-09-13 10:23:57 +02:00
|
|
|
async update(
|
2021-07-07 10:46:50 +02:00
|
|
|
project: string,
|
|
|
|
data: FeatureToggleDTO,
|
|
|
|
): Promise<FeatureToggle> {
|
|
|
|
const row = await this.db(TABLE)
|
|
|
|
.where({ name: data.name })
|
2023-12-22 14:33:16 +01:00
|
|
|
.update(this.dtoToUpdateRow(project, data))
|
2021-07-07 10:46:50 +02:00
|
|
|
.returning(FEATURE_COLUMNS);
|
#4205: mark potentially stale features (#4217)
This PR lays most of the groundwork required for emitting events when
features are marked as potentially stale by Unleash. It does **not**
emit any events just yet. The summary is:
- periodically look for features that are potentially stale and mark
them (set to run every 10 seconds for now; can be changed)
- when features are updated, if the update data contains changes to the
feature's type or createdAt date, also update the potentially stale
status.
It is currently about 220 lines of tests and about 100 lines of
application code (primarily db migration and two new methods on the
IFeatureToggleStore interface).
The reason I wanted to put this into a single PR (instead of just the db
migration, then just the potentially stale marking, then the update
logic) is:
If users get the db migration first, but not the rest of the update
logic until the events are fired, then they could get a bunch of new
events for features that should have been marked as potentially stale
several days/weeks/months ago. That seemed undesirable to me, so I
decided to bunch those changes together. Of course, I'd be happy to
break it into smaller parts.
## Rules
A toggle will be marked as potentially stale iff:
- it is not already stale
- its createdAt date is older than its feature type's expected lifetime
would dictate
## Migration
The migration adds a new `potentially_stale` column to the features
table and sets this to true for any toggles that have exceeded their
expected lifetime and that have not already been marked as `stale`.
## Discussion
### The `currentTime` parameter of `markPotentiallyStaleFeatures`
The `markPotentiallyStaleFetaures` method takes an optional
`currentTime` parameter. This was added to make it easier to test (so
you can test "into the future"), but it's not used in the application.
We can rewrite the tests to instead update feature toggles manually, but
that wouldn't test the actual marking method. Happy to discuss.
2023-07-13 14:02:33 +02:00
|
|
|
|
2023-07-17 09:53:32 +02:00
|
|
|
return this.rowToFeature(row[0]);
|
2021-07-07 10:46:50 +02:00
|
|
|
}
|
|
|
|
|
2021-09-13 10:23:57 +02:00
|
|
|
async archive(name: string): Promise<FeatureToggle> {
|
2022-07-01 13:51:26 +02:00
|
|
|
const now = new Date();
|
2021-07-07 10:46:50 +02:00
|
|
|
const row = await this.db(TABLE)
|
|
|
|
.where({ name })
|
2022-07-01 13:51:26 +02:00
|
|
|
.update({ archived_at: now })
|
2021-07-07 10:46:50 +02:00
|
|
|
.returning(FEATURE_COLUMNS);
|
|
|
|
return this.rowToFeature(row[0]);
|
|
|
|
}
|
|
|
|
|
2023-03-14 09:48:29 +01:00
|
|
|
async batchArchive(names: string[]): Promise<FeatureToggle[]> {
|
|
|
|
const now = new Date();
|
|
|
|
const rows = await this.db(TABLE)
|
|
|
|
.whereIn('name', names)
|
|
|
|
.update({ archived_at: now })
|
|
|
|
.returning(FEATURE_COLUMNS);
|
|
|
|
return rows.map((row) => this.rowToFeature(row));
|
|
|
|
}
|
|
|
|
|
2023-03-15 07:37:06 +01:00
|
|
|
async batchStale(
|
|
|
|
names: string[],
|
|
|
|
stale: boolean,
|
|
|
|
): Promise<FeatureToggle[]> {
|
|
|
|
const rows = await this.db(TABLE)
|
|
|
|
.whereIn('name', names)
|
|
|
|
.update({ stale })
|
|
|
|
.returning(FEATURE_COLUMNS);
|
|
|
|
return rows.map((row) => this.rowToFeature(row));
|
|
|
|
}
|
|
|
|
|
2021-08-12 15:04:37 +02:00
|
|
|
async delete(name: string): Promise<void> {
|
2021-07-07 10:46:50 +02:00
|
|
|
await this.db(TABLE)
|
2022-07-01 13:51:26 +02:00
|
|
|
.where({ name }) // Feature toggle must be archived to allow deletion
|
|
|
|
.whereNotNull('archived_at')
|
2021-07-07 10:46:50 +02:00
|
|
|
.del();
|
|
|
|
}
|
|
|
|
|
2023-03-15 14:08:08 +01:00
|
|
|
async batchDelete(names: string[]): Promise<void> {
|
|
|
|
await this.db(TABLE)
|
|
|
|
.whereIn('name', names)
|
|
|
|
.whereNotNull('archived_at')
|
|
|
|
.del();
|
|
|
|
}
|
|
|
|
|
2021-09-13 10:23:57 +02:00
|
|
|
async revive(name: string): Promise<FeatureToggle> {
|
2021-07-07 10:46:50 +02:00
|
|
|
const row = await this.db(TABLE)
|
|
|
|
.where({ name })
|
2022-07-01 13:51:26 +02:00
|
|
|
.update({ archived_at: null })
|
2021-07-07 10:46:50 +02:00
|
|
|
.returning(FEATURE_COLUMNS);
|
2023-10-13 09:38:18 +02:00
|
|
|
|
2021-07-07 10:46:50 +02:00
|
|
|
return this.rowToFeature(row[0]);
|
|
|
|
}
|
2021-11-24 13:08:04 +01:00
|
|
|
|
2023-03-16 08:51:18 +01:00
|
|
|
async batchRevive(names: string[]): Promise<FeatureToggle[]> {
|
|
|
|
const rows = await this.db(TABLE)
|
|
|
|
.whereIn('name', names)
|
|
|
|
.update({ archived_at: null })
|
|
|
|
.returning(FEATURE_COLUMNS);
|
2023-10-13 09:38:18 +02:00
|
|
|
|
2023-03-16 08:51:18 +01:00
|
|
|
return rows.map((row) => this.rowToFeature(row));
|
|
|
|
}
|
|
|
|
|
2023-10-13 09:38:18 +02:00
|
|
|
async disableAllEnvironmentsForFeatures(names: string[]): Promise<void> {
|
|
|
|
await this.db(FEATURE_ENVIRONMENTS_TABLE)
|
|
|
|
.whereIn('feature_name', names)
|
|
|
|
.update({ enabled: false });
|
|
|
|
}
|
|
|
|
|
2021-11-24 13:08:04 +01:00
|
|
|
async getVariants(featureName: string): Promise<IVariant[]> {
|
2022-11-21 10:37:16 +01:00
|
|
|
if (!(await this.exists(featureName))) {
|
|
|
|
throw new NotFoundError('No feature toggle found');
|
|
|
|
}
|
|
|
|
const row = await this.db(`${TABLE} as f`)
|
|
|
|
.select('fe.variants')
|
|
|
|
.join(
|
|
|
|
`${FEATURE_ENVIRONMENTS_TABLE} as fe`,
|
|
|
|
'fe.feature_name',
|
|
|
|
'f.name',
|
|
|
|
)
|
|
|
|
.where({ name: featureName })
|
|
|
|
.limit(1);
|
|
|
|
|
|
|
|
return this.rowToEnvVariants(row);
|
|
|
|
}
|
|
|
|
|
|
|
|
async getVariantsForEnv(
|
|
|
|
featureName: string,
|
|
|
|
environment: string,
|
|
|
|
): Promise<IVariant[]> {
|
|
|
|
const row = await this.db(`${TABLE} as f`)
|
|
|
|
.select('fev.variants')
|
|
|
|
.join(
|
|
|
|
`${FEATURE_ENVIRONMENTS_TABLE} as fev`,
|
|
|
|
'fev.feature_name',
|
|
|
|
'f.name',
|
|
|
|
)
|
|
|
|
.where({ name: featureName })
|
|
|
|
.andWhere({ environment });
|
|
|
|
|
|
|
|
return this.rowToEnvVariants(row);
|
2021-11-24 13:08:04 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
async saveVariants(
|
2021-12-16 11:07:19 +01:00
|
|
|
project: string,
|
2021-11-24 13:08:04 +01:00
|
|
|
featureName: string,
|
|
|
|
newVariants: IVariant[],
|
|
|
|
): Promise<FeatureToggle> {
|
2022-11-21 10:37:16 +01:00
|
|
|
const variantsString = JSON.stringify(newVariants);
|
|
|
|
await this.db('feature_environments')
|
|
|
|
.update('variants', variantsString)
|
|
|
|
.where('feature_name', featureName);
|
|
|
|
|
2023-12-01 10:20:24 +01:00
|
|
|
const row = await this.db(TABLE).select(FEATURE_COLUMNS).where({
|
|
|
|
project: project,
|
|
|
|
name: featureName,
|
|
|
|
});
|
2022-11-21 10:37:16 +01:00
|
|
|
|
|
|
|
const toggle = this.rowToFeature(row[0]);
|
|
|
|
toggle.variants = newVariants;
|
|
|
|
|
|
|
|
return toggle;
|
2021-11-24 13:08:04 +01:00
|
|
|
}
|
#4205: mark potentially stale features (#4217)
This PR lays most of the groundwork required for emitting events when
features are marked as potentially stale by Unleash. It does **not**
emit any events just yet. The summary is:
- periodically look for features that are potentially stale and mark
them (set to run every 10 seconds for now; can be changed)
- when features are updated, if the update data contains changes to the
feature's type or createdAt date, also update the potentially stale
status.
It is currently about 220 lines of tests and about 100 lines of
application code (primarily db migration and two new methods on the
IFeatureToggleStore interface).
The reason I wanted to put this into a single PR (instead of just the db
migration, then just the potentially stale marking, then the update
logic) is:
If users get the db migration first, but not the rest of the update
logic until the events are fired, then they could get a bunch of new
events for features that should have been marked as potentially stale
several days/weeks/months ago. That seemed undesirable to me, so I
decided to bunch those changes together. Of course, I'd be happy to
break it into smaller parts.
## Rules
A toggle will be marked as potentially stale iff:
- it is not already stale
- its createdAt date is older than its feature type's expected lifetime
would dictate
## Migration
The migration adds a new `potentially_stale` column to the features
table and sets this to true for any toggles that have exceeded their
expected lifetime and that have not already been marked as `stale`.
## Discussion
### The `currentTime` parameter of `markPotentiallyStaleFeatures`
The `markPotentiallyStaleFetaures` method takes an optional
`currentTime` parameter. This was added to make it easier to test (so
you can test "into the future"), but it's not used in the application.
We can rewrite the tests to instead update feature toggles manually, but
that wouldn't test the actual marking method. Happy to discuss.
2023-07-13 14:02:33 +02:00
|
|
|
|
2023-12-01 10:20:24 +01:00
|
|
|
async updatePotentiallyStaleFeatures(currentTime?: string): Promise<
|
|
|
|
{
|
|
|
|
name: string;
|
|
|
|
potentiallyStale: boolean;
|
|
|
|
project: string;
|
|
|
|
}[]
|
|
|
|
> {
|
2023-07-17 09:53:32 +02:00
|
|
|
const query = this.db.raw(
|
2023-12-01 10:20:24 +01:00
|
|
|
`SELECT name,
|
|
|
|
project,
|
|
|
|
potentially_stale,
|
|
|
|
(? > (features.created_at + ((SELECT feature_types.lifetime_days
|
|
|
|
FROM feature_types
|
|
|
|
WHERE feature_types.id = features.type) *
|
|
|
|
INTERVAL '1 day'))) as current_staleness
|
|
|
|
FROM features
|
2024-02-07 11:53:25 +01:00
|
|
|
WHERE NOT stale = true AND archived_at IS NULL`,
|
2023-07-17 09:53:32 +02:00
|
|
|
[currentTime || this.db.fn.now()],
|
|
|
|
);
|
|
|
|
|
|
|
|
const featuresToUpdate = (await query).rows
|
|
|
|
.filter(
|
|
|
|
({ potentially_stale, current_staleness }) =>
|
|
|
|
(potentially_stale ?? false) !==
|
|
|
|
(current_staleness ?? false),
|
#4205: mark potentially stale features (#4217)
This PR lays most of the groundwork required for emitting events when
features are marked as potentially stale by Unleash. It does **not**
emit any events just yet. The summary is:
- periodically look for features that are potentially stale and mark
them (set to run every 10 seconds for now; can be changed)
- when features are updated, if the update data contains changes to the
feature's type or createdAt date, also update the potentially stale
status.
It is currently about 220 lines of tests and about 100 lines of
application code (primarily db migration and two new methods on the
IFeatureToggleStore interface).
The reason I wanted to put this into a single PR (instead of just the db
migration, then just the potentially stale marking, then the update
logic) is:
If users get the db migration first, but not the rest of the update
logic until the events are fired, then they could get a bunch of new
events for features that should have been marked as potentially stale
several days/weeks/months ago. That seemed undesirable to me, so I
decided to bunch those changes together. Of course, I'd be happy to
break it into smaller parts.
## Rules
A toggle will be marked as potentially stale iff:
- it is not already stale
- its createdAt date is older than its feature type's expected lifetime
would dictate
## Migration
The migration adds a new `potentially_stale` column to the features
table and sets this to true for any toggles that have exceeded their
expected lifetime and that have not already been marked as `stale`.
## Discussion
### The `currentTime` parameter of `markPotentiallyStaleFeatures`
The `markPotentiallyStaleFetaures` method takes an optional
`currentTime` parameter. This was added to make it easier to test (so
you can test "into the future"), but it's not used in the application.
We can rewrite the tests to instead update feature toggles manually, but
that wouldn't test the actual marking method. Happy to discuss.
2023-07-13 14:02:33 +02:00
|
|
|
)
|
2023-07-19 15:20:18 +02:00
|
|
|
.map(({ current_staleness, name, project }) => ({
|
2023-07-17 09:53:32 +02:00
|
|
|
potentiallyStale: current_staleness ?? false,
|
|
|
|
name,
|
2023-07-19 15:20:18 +02:00
|
|
|
project,
|
2023-07-17 09:53:32 +02:00
|
|
|
}));
|
#4205: mark potentially stale features (#4217)
This PR lays most of the groundwork required for emitting events when
features are marked as potentially stale by Unleash. It does **not**
emit any events just yet. The summary is:
- periodically look for features that are potentially stale and mark
them (set to run every 10 seconds for now; can be changed)
- when features are updated, if the update data contains changes to the
feature's type or createdAt date, also update the potentially stale
status.
It is currently about 220 lines of tests and about 100 lines of
application code (primarily db migration and two new methods on the
IFeatureToggleStore interface).
The reason I wanted to put this into a single PR (instead of just the db
migration, then just the potentially stale marking, then the update
logic) is:
If users get the db migration first, but not the rest of the update
logic until the events are fired, then they could get a bunch of new
events for features that should have been marked as potentially stale
several days/weeks/months ago. That seemed undesirable to me, so I
decided to bunch those changes together. Of course, I'd be happy to
break it into smaller parts.
## Rules
A toggle will be marked as potentially stale iff:
- it is not already stale
- its createdAt date is older than its feature type's expected lifetime
would dictate
## Migration
The migration adds a new `potentially_stale` column to the features
table and sets this to true for any toggles that have exceeded their
expected lifetime and that have not already been marked as `stale`.
## Discussion
### The `currentTime` parameter of `markPotentiallyStaleFeatures`
The `markPotentiallyStaleFetaures` method takes an optional
`currentTime` parameter. This was added to make it easier to test (so
you can test "into the future"), but it's not used in the application.
We can rewrite the tests to instead update feature toggles manually, but
that wouldn't test the actual marking method. Happy to discuss.
2023-07-13 14:02:33 +02:00
|
|
|
|
2023-07-17 09:53:32 +02:00
|
|
|
await this.db(TABLE)
|
|
|
|
.update('potentially_stale', true)
|
|
|
|
.whereIn(
|
|
|
|
'name',
|
|
|
|
featuresToUpdate
|
|
|
|
.filter((feature) => feature.potentiallyStale === true)
|
|
|
|
.map((feature) => feature.name),
|
|
|
|
);
|
|
|
|
|
|
|
|
await this.db(TABLE)
|
|
|
|
.update('potentially_stale', false)
|
|
|
|
.whereIn(
|
|
|
|
'name',
|
|
|
|
featuresToUpdate
|
|
|
|
.filter((feature) => feature.potentiallyStale !== true)
|
|
|
|
.map((feature) => feature.name),
|
|
|
|
);
|
|
|
|
|
|
|
|
return featuresToUpdate;
|
#4205: mark potentially stale features (#4217)
This PR lays most of the groundwork required for emitting events when
features are marked as potentially stale by Unleash. It does **not**
emit any events just yet. The summary is:
- periodically look for features that are potentially stale and mark
them (set to run every 10 seconds for now; can be changed)
- when features are updated, if the update data contains changes to the
feature's type or createdAt date, also update the potentially stale
status.
It is currently about 220 lines of tests and about 100 lines of
application code (primarily db migration and two new methods on the
IFeatureToggleStore interface).
The reason I wanted to put this into a single PR (instead of just the db
migration, then just the potentially stale marking, then the update
logic) is:
If users get the db migration first, but not the rest of the update
logic until the events are fired, then they could get a bunch of new
events for features that should have been marked as potentially stale
several days/weeks/months ago. That seemed undesirable to me, so I
decided to bunch those changes together. Of course, I'd be happy to
break it into smaller parts.
## Rules
A toggle will be marked as potentially stale iff:
- it is not already stale
- its createdAt date is older than its feature type's expected lifetime
would dictate
## Migration
The migration adds a new `potentially_stale` column to the features
table and sets this to true for any toggles that have exceeded their
expected lifetime and that have not already been marked as `stale`.
## Discussion
### The `currentTime` parameter of `markPotentiallyStaleFeatures`
The `markPotentiallyStaleFetaures` method takes an optional
`currentTime` parameter. This was added to make it easier to test (so
you can test "into the future"), but it's not used in the application.
We can rewrite the tests to instead update feature toggles manually, but
that wouldn't test the actual marking method. Happy to discuss.
2023-07-13 14:02:33 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
async isPotentiallyStale(featureName: string): Promise<boolean> {
|
|
|
|
const result = await this.db(TABLE)
|
|
|
|
.first(['potentially_stale'])
|
|
|
|
.from(TABLE)
|
|
|
|
.where({ name: featureName });
|
|
|
|
|
|
|
|
return result?.potentially_stale ?? false;
|
|
|
|
}
|
2024-01-25 13:09:30 +01:00
|
|
|
|
2024-01-31 12:30:42 +01:00
|
|
|
async setCreatedByUserId(batchSize: number): Promise<number | undefined> {
|
2024-01-25 13:09:30 +01:00
|
|
|
const EVENTS_TABLE = 'events';
|
|
|
|
const USERS_TABLE = 'users';
|
|
|
|
const API_TOKEN_TABLE = 'api_tokens';
|
|
|
|
|
|
|
|
if (!this.flagResolver.isEnabled('createdByUserIdDataMigration')) {
|
2024-01-31 12:30:42 +01:00
|
|
|
return undefined;
|
2024-01-25 13:09:30 +01:00
|
|
|
}
|
|
|
|
const toUpdate = await this.db(`${TABLE} as f`)
|
|
|
|
.joinRaw(`JOIN ${EVENTS_TABLE} AS ev ON ev.feature_name = f.name`)
|
|
|
|
.joinRaw(
|
|
|
|
`LEFT OUTER JOIN ${USERS_TABLE} AS u on ev.created_by = u.username OR ev.created_by = u.email`,
|
|
|
|
)
|
|
|
|
.joinRaw(
|
|
|
|
`LEFT OUTER JOIN ${API_TOKEN_TABLE} AS t on ev.created_by = t.username`,
|
|
|
|
)
|
|
|
|
.whereRaw(
|
2024-01-29 15:14:44 +01:00
|
|
|
`f.created_by_user_id IS null AND
|
|
|
|
ev.type = 'feature-created' AND
|
|
|
|
(u.id IS NOT null OR t.username IS NOT null)`,
|
2024-01-25 13:09:30 +01:00
|
|
|
)
|
2024-01-29 15:14:44 +01:00
|
|
|
.orderBy('f.created_at', 'desc')
|
2024-01-25 13:09:30 +01:00
|
|
|
.limit(batchSize)
|
|
|
|
.select(['f.*', 'ev.created_by', 'u.id', 't.username']);
|
|
|
|
|
2024-01-29 15:14:44 +01:00
|
|
|
const updatePromises = toUpdate.map((row) => {
|
|
|
|
const id = row.id || ADMIN_TOKEN_USER.id;
|
2024-01-25 13:09:30 +01:00
|
|
|
|
2024-01-29 15:14:44 +01:00
|
|
|
return this.db(TABLE)
|
|
|
|
.update({ created_by_user_id: id })
|
|
|
|
.where({ name: row.name });
|
|
|
|
});
|
2024-01-29 08:07:33 +01:00
|
|
|
|
|
|
|
await Promise.all(updatePromises);
|
2024-01-31 12:30:42 +01:00
|
|
|
return toUpdate.length;
|
2024-01-25 13:09:30 +01:00
|
|
|
}
|
2021-07-07 10:46:50 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
module.exports = FeatureToggleStore;
|