1
0
mirror of https://github.com/Unleash/unleash.git synced 2025-08-27 13:49:10 +02:00
This commit is contained in:
Mateusz Kwasniewski 2025-07-31 12:04:58 -03:00 committed by GitHub
commit 5e553cb960
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 107 additions and 40 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -87,11 +87,11 @@ export default class FakeFeatureToggleStore implements IFeatureToggleStore {
return this.features.filter((f) => names.includes(f.name)); 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) { 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>) { private getFilterQuery(query: Partial<IFeatureToggleStoreQuery>) {

View File

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

View File

@ -301,14 +301,14 @@ export class FeatureToggleService {
featureName, featureName,
projectId, projectId,
}: IFeatureContext): Promise<void> { }: 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( throw new NotFoundError(
`There's no feature named "${featureName}" in project "${projectId}"${ `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}"`
}`, }`,
); );
} }
@ -1118,12 +1118,14 @@ export class FeatureToggleService {
environmentVariants, environmentVariants,
userId, userId,
}: IGetFeatureParams): Promise<FeatureToggleView> { }: IGetFeatureParams): Promise<FeatureToggleView> {
console.log('get feature');
if (projectId) { if (projectId) {
await this.validateFeatureBelongsToProject({ await this.validateFeatureBelongsToProject({
featureName, featureName,
projectId, projectId,
}); });
} }
console.log('get feature found');
let dependencies: IDependency[] = []; let dependencies: IDependency[] = [];
let children: string[] = []; let children: string[] = [];
@ -2184,10 +2186,6 @@ export class FeatureToggleService {
); );
} }
async getProjectId(name: string): Promise<string | undefined> {
return this.featureToggleStore.getProjectId(name);
}
async updateFeatureStrategyProject( async updateFeatureStrategyProject(
featureName: string, featureName: string,
newProjectId: string, newProjectId: string,

View File

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

View File

@ -20,10 +20,13 @@ export class FeaturesReadModel implements IFeaturesReadModel {
async featuresInTheSameProject( async featuresInTheSameProject(
featureA: string, featureA: string,
featureB: string, featureB: string,
project: string,
): Promise<boolean> { ): Promise<boolean> {
const rows = await this.db('features') const rows = await this.db('feature_project')
.countDistinct('project as count') .whereIn('feature_name', [featureA, featureB])
.whereIn('name', [featureA, featureB]); .andWhere('project_id', project)
return Number(rows[0].count) === 1; .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 () => { test('should not crash for unknown toggle', async () => {
const project = await featureToggleStore.getProjectId( const project = await featureToggleStore.getProjectIds(
'missing-toggle-name', 'missing-toggle-name',
); );
expect(project).toBe(undefined); expect(project).toBe([]);
}); });
test('should not crash for undefined toggle name', async () => { test('should not crash for undefined toggle name', async () => {
const project = await featureToggleStore.getProjectId(undefined); const project = await featureToggleStore.getProjectIds(undefined);
expect(project).toBe(undefined); expect(project).toBe([]);
}); });
describe('potentially_stale marking', () => { describe('potentially_stale marking', () => {

View File

@ -22,7 +22,7 @@ export interface IFeatureToggleStore extends Store<FeatureToggle, string> {
setLastSeen(data: LastSeenInput[]): Promise<void>; 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>; create(project: string, data: FeatureToggleInsert): Promise<FeatureToggle>;

View File

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

View File

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