1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-08-09 13:47:13 +02:00

feat: shared flags

This commit is contained in:
kwasniew 2025-05-16 09:32:24 +02:00
parent a2723ec0c0
commit f90aef2167
No known key found for this signature in database
GPG Key ID: 43A7CBC24C119560
14 changed files with 107 additions and 40 deletions

View File

@ -74,7 +74,7 @@ export default class FeatureToggleClientStore
'features.name as name',
'features.description as description',
'features.type as type',
'features.project as project',
'feature_project.project_id as project',
'features.stale as stale',
'features.impression_data as impression_data',
'features.last_seen_at as last_seen_at',
@ -100,6 +100,11 @@ export default class FeatureToggleClientStore
] as (string | Raw<any>)[];
let query = this.db('features')
.join(
'feature_project',
'feature_project.feature_name',
'features.name',
)
.modify(FeatureToggleStore.filterByArchived, archived)
.leftJoin(
this.db('feature_strategies')
@ -173,7 +178,10 @@ export default class FeatureToggleClientStore
featureQuery.project &&
!featureQuery.project.includes(ALL_PROJECTS)
) {
query = query.whereIn('project', featureQuery.project);
query = query.whereIn(
'feature_project.project_id',
featureQuery.project,
);
}
if (featureQuery.namePrefix) {
query = query.where(

View File

@ -71,12 +71,17 @@ export class DependentFeaturesReadModel implements IDependentFeaturesReadModel {
return [];
}
const rows = await this.db('features')
.innerJoin(
'feature_project',
'feature_project.feature_name',
'features.name',
)
.leftJoin(
'dependent_features',
'features.name',
'dependent_features.child',
)
.where('features.project', result[0].project)
.where('feature_project.project_id', result[0].project)
.andWhere('features.name', '!=', child)
.andWhere('dependent_features.child', null)
.andWhere('features.archived_at', null)

View File

@ -108,7 +108,11 @@ export class DependentFeaturesService {
this.dependentFeaturesReadModel.getChildren([child]),
this.dependentFeaturesReadModel.getParents(parent),
this.featuresReadModel.featureExists(parent),
this.featuresReadModel.featuresInTheSameProject(child, parent),
this.featuresReadModel.featuresInTheSameProject(
child,
parent,
projectId,
),
]);
if (grandchildren.length > 0) {

View File

@ -117,7 +117,7 @@ class FeatureSearchStore implements IFeatureSearchStore {
'features.description as description',
'features.type as type',
'features.archived_at as archived_at',
'features.project as project',
'feature_project.project_id as project',
'features.created_at as created_at',
'features.stale as stale',
'features.last_seen_at as last_seen_at',
@ -144,6 +144,12 @@ class FeatureSearchStore implements IFeatureSearchStore {
const lastSeenQuery = 'last_seen_at_metrics.last_seen_at';
selectColumns.push(`${lastSeenQuery} as env_last_seen_at`);
query.innerJoin(
'feature_project',
'features.name',
'feature_project.feature_name',
);
if (userId) {
query.leftJoin(`favorite_features`, function () {
this.on(
@ -532,7 +538,7 @@ class FeatureSearchStore implements IFeatureSearchStore {
name: 'features.name',
type: 'features.type',
stale: 'features.stale',
project: 'features.project',
project: 'feature_project.project_id',
};
let rankingSql = 'order by ';
@ -786,9 +792,14 @@ const applyQueryParams = (
const segmentConditions = queryParams.filter(
(param) => param.field === 'segment',
);
const genericConditions = queryParams.filter(
(param) => !['tag', 'stale'].includes(param.field),
);
const genericConditions = queryParams
.filter((param) => !['tag', 'stale'].includes(param.field))
.map((params) =>
params.field === 'project'
? { ...params, field: 'feature_project.project_id' }
: params,
);
applyGenericQueryParams(query, genericConditions);
applyStaleConditions(query, staleConditions);

View File

@ -87,11 +87,11 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore {
return this.features.filter((f) => names.includes(f.name));
}
async getProjectId(name: string | undefined): Promise<string | undefined> {
async getProjectIds(name: string | undefined): Promise<string[]> {
if (name === undefined) {
return Promise.resolve(undefined);
return Promise.resolve([]);
}
return Promise.resolve(this.get(name).then((f) => f.project));
return Promise.resolve(this.get(name).then((f) => [f.project]));
}
private getFilterQuery(query: Partial<IFeatureToggleStoreQuery>) {

View File

@ -8,6 +8,7 @@ export class FakeFeaturesReadModel implements IFeaturesReadModel {
featuresInTheSameProject(
featureA: string,
featureB: string,
project: string,
): Promise<boolean> {
return Promise.resolve(true);
}

View File

@ -304,14 +304,14 @@ export class FeatureToggleService {
featureName,
projectId,
}: IFeatureContext): Promise<void> {
const id = await this.featureToggleStore.getProjectId(featureName);
const ids = await this.featureToggleStore.getProjectIds(featureName);
if (id !== projectId) {
if (ids.includes(projectId)) {
throw new NotFoundError(
`There's no feature named "${featureName}" in project "${projectId}"${
id === undefined
ids.length > 0
? '.'
: `, but there's a feature with that name in project "${id}"`
: `, but there's a feature with that name in project "${ids}"`
}`,
);
}
@ -1092,12 +1092,14 @@ export class FeatureToggleService {
environmentVariants,
userId,
}: IGetFeatureParams): Promise<FeatureToggleView> {
console.log('get feature');
if (projectId) {
await this.validateFeatureBelongsToProject({
featureName,
projectId,
});
}
console.log('get feature found');
let dependencies: IDependency[] = [];
let children: string[] = [];
@ -2170,10 +2172,6 @@ export class FeatureToggleService {
);
}
async getProjectId(name: string): Promise<string | undefined> {
return this.featureToggleStore.getProjectId(name);
}
async updateFeatureStrategyProject(
featureName: string,
newProjectId: string,

View File

@ -322,16 +322,20 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
* @deprecated
* @param name
*/
async getProjectId(name: string): Promise<string> {
return this.db
.first(['project'])
.from(TABLE)
.where({ name })
.then((r) => (r ? r.project : undefined))
.catch((e) => {
this.logger.error(e);
return undefined;
});
async getProjectIds(name: string): Promise<string[]> {
return (
this.db
.select(['project_id'])
.from('feature_project')
// .join('feature_project', 'feature_project.feature_name', 'feature.name')
// .where({ name })
.where('feature_project.feature_name', name)
);
// .then((r) => (r ? r.project : undefined))
// .catch((e) => {
// this.logger.error(e);
// return undefined;
// });
}
async exists(name: string): Promise<boolean> {
@ -480,6 +484,11 @@ export default class FeatureToggleStore implements IFeatureToggleStore {
.insert(this.insertToRow(project, data))
.returning(FEATURE_COLUMNS);
await this.db('feature_project').insert({
feature_name: data.name,
project_id: project,
});
return this.rowToFeature(row[0]);
} catch (err) {
this.logger.error('Could not insert feature, error: ', err);

View File

@ -20,10 +20,13 @@ export class FeaturesReadModel implements IFeaturesReadModel {
async featuresInTheSameProject(
featureA: string,
featureB: string,
project: string,
): Promise<boolean> {
const rows = await this.db('features')
.countDistinct('project as count')
.whereIn('name', [featureA, featureB]);
return Number(rows[0].count) === 1;
const rows = await this.db('feature_project')
.whereIn('feature_name', [featureA, featureB])
.andWhere('project_id', project)
.countDistinct('feature_name as count');
return Number(rows[0].count) === 2;
}
}

View File

@ -27,15 +27,15 @@ afterAll(async () => {
});
test('should not crash for unknown toggle', async () => {
const project = await featureToggleStore.getProjectId(
const project = await featureToggleStore.getProjectIds(
'missing-toggle-name',
);
expect(project).toBe(undefined);
expect(project).toBe([]);
});
test('should not crash for undefined toggle name', async () => {
const project = await featureToggleStore.getProjectId(undefined);
expect(project).toBe(undefined);
const project = await featureToggleStore.getProjectIds(undefined);
expect(project).toBe([]);
});
describe('potentially_stale marking', () => {

View File

@ -23,7 +23,7 @@ export interface IFeatureToggleStore extends Store<FeatureToggle, string> {
setLastSeen(data: LastSeenInput[]): Promise<void>;
getProjectId(name: string | undefined): Promise<string | undefined>;
getProjectIds(name: string | undefined): Promise<string[]>;
create(project: string, data: FeatureToggleInsert): Promise<FeatureToggle>;

View File

@ -3,5 +3,6 @@ export interface IFeaturesReadModel {
featuresInTheSameProject(
featureA: string,
featureB: string,
project: string,
): Promise<boolean>;
}

View File

@ -87,7 +87,9 @@ const rbacMiddleware = (
)
) {
const { featureName } = params;
projectId = await featureToggleStore.getProjectId(featureName);
projectId = (
await featureToggleStore.getProjectIds(featureName)
)[0];
} else if (
projectId === undefined &&
permissionsArray.some(

View File

@ -0,0 +1,25 @@
exports.up = (db, callback) => {
db.runSql(
`
CREATE TABLE IF NOT EXISTS feature_project (
feature_name VARCHAR(255) NOT NULL,
project_id VARCHAR(255) NOT NULL,
PRIMARY KEY (feature_name, project_id),
FOREIGN KEY (feature_name) REFERENCES features(name) ON DELETE CASCADE,
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
);
INSERT INTO feature_project (feature_name, project_id)
SELECT name, project
FROM features
WHERE project IS NOT NULL;
`,
callback,
);
};
exports.down = (db, callback) => {
db.runSql(
`DROP TABLE IF EXISTS feature_project`,
callback,
);
};